From cdc08578785c6c550b151882e8eefa3d469967ce Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 20:55:05 -0500 Subject: [PATCH 01/45] adding things --- .github/workflows/ci.yml | 33 +++++++++++++++++++++++++++++++++ tests/test_first_test.py | 2 ++ 2 files changed, 35 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 tests/test_first_test.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..24a8602 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: Python CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: checkout + uses: [actions/checkout](github.com)@v4 + - name: Set up Python + uses: [actions/setup-python](github.com)@v5 + with: + python-version: '3.14' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install poetry + poetry install + - name: Run tests with coverage + run: | + pytest --cov=f1p --cov-report=xml:coverage.xml --cov-report=term ./tests + - name: Upload coverage reports to GitHub + uses: [actions/upload-artifact](github.com)@v4 + with: + name: code-coverage-report + path: coverage.xml diff --git a/tests/test_first_test.py b/tests/test_first_test.py new file mode 100644 index 0000000..50a8110 --- /dev/null +++ b/tests/test_first_test.py @@ -0,0 +1,2 @@ +def test_first_test() -> None: + assert True From db3a2b7dc7a0f023ee59cdc0cbe4bdfe8da8f54e Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 20:56:31 -0500 Subject: [PATCH 02/45] add blank line --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24a8602..d3019d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ on: jobs: build: runs-on: ubuntu-latest + steps: - name: checkout uses: [actions/checkout](github.com)@v4 From 7b1cb33179c3a4ecf3462fb6964523c9570cad6d Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 20:58:02 -0500 Subject: [PATCH 03/45] update --- .github/workflows/ci.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3019d2..a96ffa7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,12 +11,11 @@ on: jobs: build: runs-on: ubuntu-latest - + steps: - - name: checkout - uses: [actions/checkout](github.com)@v4 + - uses: actions/checkout@v5 - name: Set up Python - uses: [actions/setup-python](github.com)@v5 + uses: actions/setup-python@v5 with: python-version: '3.14' - name: Install dependencies From 17641ccab399b9b195b5481480a7bc559924ee4c Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 20:59:55 -0500 Subject: [PATCH 04/45] use poetry --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a96ffa7..3432d60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: poetry install - name: Run tests with coverage run: | - pytest --cov=f1p --cov-report=xml:coverage.xml --cov-report=term ./tests + poetry run pytest ./tests --cov=f1p --cov-report=xml:coverage.xml --cov-report=term - name: Upload coverage reports to GitHub uses: [actions/upload-artifact](github.com)@v4 with: From 98af5a9f3b442af79e06278252e55df6d9da581a Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 21:00:14 -0500 Subject: [PATCH 05/45] fix uses --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3432d60..9940351 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: run: | poetry run pytest ./tests --cov=f1p --cov-report=xml:coverage.xml --cov-report=term - name: Upload coverage reports to GitHub - uses: [actions/upload-artifact](github.com)@v4 + uses: actions/upload-artifact@v5 with: name: code-coverage-report path: coverage.xml From 6bc6410296088fae61e7c2cfaac22a85fd0e8c66 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 21:03:33 -0500 Subject: [PATCH 06/45] updated coverage --- .github/workflows/ci.yml | 2 +- .gitignore | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9940351..b592902 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: poetry install - name: Run tests with coverage run: | - poetry run pytest ./tests --cov=f1p --cov-report=xml:coverage.xml --cov-report=term + poetry run pytest ./tests --cov=./src --cov-report=xml:coverage.xml --cov-report=term - name: Upload coverage reports to GitHub uses: actions/upload-artifact@v5 with: diff --git a/.gitignore b/.gitignore index cb2d456..4d00afb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ __pycache__/ *.py[codz] *$py.class .idea -.fastf1-cache \ No newline at end of file +.fastf1-cache +.coverage +coverage.xml \ No newline at end of file From 20e65ea53ab05ddd6f9b2e8fc53743e0490954f4 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 21:06:00 -0500 Subject: [PATCH 07/45] show results --- .github/workflows/ci.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b592902..2be2c4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Python CI +name: Unit Tests on: push: @@ -31,3 +31,8 @@ jobs: with: name: code-coverage-report path: coverage.xml + - name: Pytest Coverage Comment + uses: [ MishaKav/pytest-coverage-comment ](https://github.com/marketplace/actions/pytest-coverage-comment)@main + with: + pytest-coverage-path: coverage.xml + github-token: ${{ secrets.GITHUB_TOKEN }} From 731ad39fb55b7f63ef29fbbd8cc89fdfeb2d9cc9 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 21:08:11 -0500 Subject: [PATCH 08/45] fix syntax --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2be2c4f..74aacfd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,6 @@ jobs: path: coverage.xml - name: Pytest Coverage Comment uses: [ MishaKav/pytest-coverage-comment ](https://github.com/marketplace/actions/pytest-coverage-comment)@main - with: - pytest-coverage-path: coverage.xml - github-token: ${{ secrets.GITHUB_TOKEN }} + with: + pytest-coverage-path: coverage.xml + github-token: ${{ secrets.GITHUB_TOKEN }} From cf6af1c24e07c9043b619d2c7bfb17f0b37c2628 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 21:09:21 -0500 Subject: [PATCH 09/45] fix uses --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74aacfd..5262995 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: name: code-coverage-report path: coverage.xml - name: Pytest Coverage Comment - uses: [ MishaKav/pytest-coverage-comment ](https://github.com/marketplace/actions/pytest-coverage-comment)@main + uses: MishaKav/pytest-coverage-comment@v1.2.0 with: pytest-coverage-path: coverage.xml github-token: ${{ secrets.GITHUB_TOKEN }} From 4d86769be5236c197a51dac604c4ec635404764e Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 21:20:30 -0500 Subject: [PATCH 10/45] add proper test --- tests/f1p/__init__.py | 0 tests/f1p/services/__init__.py | 0 tests/f1p/services/data_extractor/__init__.py | 0 .../services/data_extractor/enums/__init__.py | 0 .../test_conventional_session_identifiers.py | 22 +++++++++++++++++++ tests/test_first_test.py | 2 -- 6 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 tests/f1p/__init__.py create mode 100644 tests/f1p/services/__init__.py create mode 100644 tests/f1p/services/data_extractor/__init__.py create mode 100644 tests/f1p/services/data_extractor/enums/__init__.py create mode 100644 tests/f1p/services/data_extractor/enums/test_conventional_session_identifiers.py delete mode 100644 tests/test_first_test.py diff --git a/tests/f1p/__init__.py b/tests/f1p/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/f1p/services/__init__.py b/tests/f1p/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/f1p/services/data_extractor/__init__.py b/tests/f1p/services/data_extractor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/f1p/services/data_extractor/enums/__init__.py b/tests/f1p/services/data_extractor/enums/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/f1p/services/data_extractor/enums/test_conventional_session_identifiers.py b/tests/f1p/services/data_extractor/enums/test_conventional_session_identifiers.py new file mode 100644 index 0000000..79a7a85 --- /dev/null +++ b/tests/f1p/services/data_extractor/enums/test_conventional_session_identifiers.py @@ -0,0 +1,22 @@ +from enum import Enum + +import pytest + +from f1p.services.data_extractor.enums import ConventionalSessionIdentifiers + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + ["Practice 1", ConventionalSessionIdentifiers.FREE_PRACTICE_1], + ["Practice 2", ConventionalSessionIdentifiers.FREE_PRACTICE_2], + ["Practice 3", ConventionalSessionIdentifiers.FREE_PRACTICE_3], + ["Qualifying", ConventionalSessionIdentifiers.QUALIFYING], + ["Race", ConventionalSessionIdentifiers.RACE], + ] +) +def test_initialization(value: str, expected: ConventionalSessionIdentifiers) -> None: + enum = ConventionalSessionIdentifiers(value) + + assert isinstance(enum, Enum) + assert enum == expected diff --git a/tests/test_first_test.py b/tests/test_first_test.py deleted file mode 100644 index 50a8110..0000000 --- a/tests/test_first_test.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_first_test() -> None: - assert True From 8ec215d3ec3287eeb1a722bef6a5cb4f7a13d552 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 21:25:39 -0500 Subject: [PATCH 11/45] try with new coverage --- poetry.lock | 190 ++++++++++++++++++++++++------------------------- pyproject.toml | 1 + 2 files changed, 96 insertions(+), 95 deletions(-) diff --git a/poetry.lock b/poetry.lock index 848bb39..a0831ee 100644 --- a/poetry.lock +++ b/poetry.lock @@ -438,104 +438,104 @@ test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist" [[package]] name = "coverage" -version = "7.13.0" +version = "7.13.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" -groups = ["dev"] +groups = ["main", "dev"] files = [ - {file = "coverage-7.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:02d9fb9eccd48f6843c98a37bd6817462f130b86da8660461e8f5e54d4c06070"}, - {file = "coverage-7.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:367449cf07d33dc216c083f2036bb7d976c6e4903ab31be400ad74ad9f85ce98"}, - {file = "coverage-7.13.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cdb3c9f8fef0a954c632f64328a3935988d33a6604ce4bf67ec3e39670f12ae5"}, - {file = "coverage-7.13.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d10fd186aac2316f9bbb46ef91977f9d394ded67050ad6d84d94ed6ea2e8e54e"}, - {file = "coverage-7.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f88ae3e69df2ab62fb0bc5219a597cb890ba5c438190ffa87490b315190bb33"}, - {file = "coverage-7.13.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4be718e51e86f553bcf515305a158a1cd180d23b72f07ae76d6017c3cc5d791"}, - {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a00d3a393207ae12f7c49bb1c113190883b500f48979abb118d8b72b8c95c032"}, - {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a7b1cd820e1b6116f92c6128f1188e7afe421c7e1b35fa9836b11444e53ebd9"}, - {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:37eee4e552a65866f15dedd917d5e5f3d59805994260720821e2c1b51ac3248f"}, - {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62d7c4f13102148c78d7353c6052af6d899a7f6df66a32bddcc0c0eb7c5326f8"}, - {file = "coverage-7.13.0-cp310-cp310-win32.whl", hash = "sha256:24e4e56304fdb56f96f80eabf840eab043b3afea9348b88be680ec5986780a0f"}, - {file = "coverage-7.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:74c136e4093627cf04b26a35dab8cbfc9b37c647f0502fc313376e11726ba303"}, - {file = "coverage-7.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820"}, - {file = "coverage-7.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f"}, - {file = "coverage-7.13.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96"}, - {file = "coverage-7.13.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ed5630d946859de835a85e9a43b721123a8a44ec26e2830b296d478c7fd4259"}, - {file = "coverage-7.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f15a931a668e58087bc39d05d2b4bf4b14ff2875b49c994bbdb1c2217a8daeb"}, - {file = "coverage-7.13.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30a3a201a127ea57f7e14ba43c93c9c4be8b7d17a26e03bb49e6966d019eede9"}, - {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a485ff48fbd231efa32d58f479befce52dcb6bfb2a88bb7bf9a0b89b1bc8030"}, - {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:22486cdafba4f9e471c816a2a5745337742a617fef68e890d8baf9f3036d7833"}, - {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:263c3dbccc78e2e331e59e90115941b5f53e85cfcc6b3b2fbff1fd4e3d2c6ea8"}, - {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5330fa0cc1f5c3c4c3bb8e101b742025933e7848989370a1d4c8c5e401ea753"}, - {file = "coverage-7.13.0-cp311-cp311-win32.whl", hash = "sha256:0f4872f5d6c54419c94c25dd6ae1d015deeb337d06e448cd890a1e89a8ee7f3b"}, - {file = "coverage-7.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51a202e0f80f241ccb68e3e26e19ab5b3bf0f813314f2c967642f13ebcf1ddfe"}, - {file = "coverage-7.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:d2a9d7f1c11487b1c69367ab3ac2d81b9b3721f097aa409a3191c3e90f8f3dd7"}, - {file = "coverage-7.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf"}, - {file = "coverage-7.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f"}, - {file = "coverage-7.13.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb"}, - {file = "coverage-7.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621"}, - {file = "coverage-7.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74"}, - {file = "coverage-7.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57"}, - {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8"}, - {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d"}, - {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b"}, - {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd"}, - {file = "coverage-7.13.0-cp312-cp312-win32.whl", hash = "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef"}, - {file = "coverage-7.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae"}, - {file = "coverage-7.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080"}, - {file = "coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf"}, - {file = "coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a"}, - {file = "coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74"}, - {file = "coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6"}, - {file = "coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b"}, - {file = "coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232"}, - {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971"}, - {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d"}, - {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137"}, - {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511"}, - {file = "coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1"}, - {file = "coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a"}, - {file = "coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6"}, - {file = "coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a"}, - {file = "coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8"}, - {file = "coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053"}, - {file = "coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071"}, - {file = "coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e"}, - {file = "coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493"}, - {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0"}, - {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e"}, - {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c"}, - {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e"}, - {file = "coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46"}, - {file = "coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39"}, - {file = "coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e"}, - {file = "coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256"}, - {file = "coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a"}, - {file = "coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9"}, - {file = "coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19"}, - {file = "coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be"}, - {file = "coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb"}, - {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8"}, - {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b"}, - {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9"}, - {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927"}, - {file = "coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f"}, - {file = "coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc"}, - {file = "coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b"}, - {file = "coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28"}, - {file = "coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe"}, - {file = "coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657"}, - {file = "coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff"}, - {file = "coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3"}, - {file = "coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b"}, - {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d"}, - {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e"}, - {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940"}, - {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2"}, - {file = "coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7"}, - {file = "coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc"}, - {file = "coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a"}, - {file = "coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904"}, - {file = "coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936"}, + {file = "coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147"}, + {file = "coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d"}, + {file = "coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0"}, + {file = "coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90"}, + {file = "coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d"}, + {file = "coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b"}, + {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6"}, + {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e"}, + {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae"}, + {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29"}, + {file = "coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f"}, + {file = "coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1"}, + {file = "coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88"}, + {file = "coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3"}, + {file = "coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9"}, + {file = "coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee"}, + {file = "coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf"}, + {file = "coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3"}, + {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef"}, + {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851"}, + {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb"}, + {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba"}, + {file = "coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19"}, + {file = "coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a"}, + {file = "coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c"}, + {file = "coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3"}, + {file = "coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e"}, + {file = "coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c"}, + {file = "coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62"}, + {file = "coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968"}, + {file = "coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e"}, + {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f"}, + {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee"}, + {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf"}, + {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c"}, + {file = "coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7"}, + {file = "coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6"}, + {file = "coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c"}, + {file = "coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78"}, + {file = "coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b"}, + {file = "coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd"}, + {file = "coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992"}, + {file = "coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4"}, + {file = "coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a"}, + {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766"}, + {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4"}, + {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398"}, + {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784"}, + {file = "coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461"}, + {file = "coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500"}, + {file = "coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9"}, + {file = "coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc"}, + {file = "coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a"}, + {file = "coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4"}, + {file = "coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6"}, + {file = "coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1"}, + {file = "coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd"}, + {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c"}, + {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0"}, + {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e"}, + {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53"}, + {file = "coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842"}, + {file = "coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2"}, + {file = "coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09"}, + {file = "coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894"}, + {file = "coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a"}, + {file = "coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f"}, + {file = "coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909"}, + {file = "coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4"}, + {file = "coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75"}, + {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9"}, + {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465"}, + {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864"}, + {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9"}, + {file = "coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5"}, + {file = "coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a"}, + {file = "coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0"}, + {file = "coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a"}, + {file = "coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6"}, + {file = "coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673"}, + {file = "coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5"}, + {file = "coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d"}, + {file = "coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8"}, + {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486"}, + {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564"}, + {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7"}, + {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416"}, + {file = "coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f"}, + {file = "coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79"}, + {file = "coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4"}, + {file = "coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573"}, + {file = "coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd"}, ] [package.extras] @@ -2172,4 +2172,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.13,<4.0" -content-hash = "9ff5fa3bd6eaecd809a01de5dfe7ebbd7abd1c72b4a67819c2b5fd51765a938b" +content-hash = "3215b80723503aa24b1be3a28df8e3210f4a7361baf9267ecabb530dd2b2c6d7" diff --git a/pyproject.toml b/pyproject.toml index dfbef04..59256e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ pandas-stubs = "^2.3.3.251219" [tool.poetry.group.dev.dependencies] ruff = "^0.12.8" pytest = "8.4.1" +coverage = "^7.13.1" pytest-mock = "3.14.1" pytest-cov = "6.2.1" pytest-asyncio = "1.1.0" From d36b9dd380b8bb12ca71343495fcd2e69a4999c9 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 21:28:30 -0500 Subject: [PATCH 12/45] updated poetry lock --- poetry.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index a0831ee..c7b8b5f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -442,7 +442,7 @@ version = "7.13.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" -groups = ["main", "dev"] +groups = ["dev"] files = [ {file = "coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147"}, {file = "coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d"}, @@ -2172,4 +2172,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.13,<4.0" -content-hash = "3215b80723503aa24b1be3a28df8e3210f4a7361baf9267ecabb530dd2b2c6d7" +content-hash = "125fb2dfa66bd455fd7e51ceba40cc1afa28e7b2de656779591755a7ef41360f" From 862e942962c6b899fc935ffa9e054d45bbe06a7a Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 21:32:47 -0500 Subject: [PATCH 13/45] try some different coverage runs --- .github/workflows/ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5262995..b6f7ea0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,14 +25,15 @@ jobs: poetry install - name: Run tests with coverage run: | - poetry run pytest ./tests --cov=./src --cov-report=xml:coverage.xml --cov-report=term + poetry run pytest ./tests --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=./src | tee pytest-coverage.txt - name: Upload coverage reports to GitHub uses: actions/upload-artifact@v5 with: name: code-coverage-report - path: coverage.xml + path: pytest.xml - name: Pytest Coverage Comment uses: MishaKav/pytest-coverage-comment@v1.2.0 with: - pytest-coverage-path: coverage.xml + pytest-coverage-path: ./pytest-coverage.txt + junitxml-path: ./pytest.xml github-token: ${{ secrets.GITHUB_TOKEN }} From a5e6c55040e5d1573e42c79b617f0cb654f19c34 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 21:34:18 -0500 Subject: [PATCH 14/45] adding permissions --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6f7ea0..64e597f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,10 @@ on: branches: - main +permissions: + contents: read # For checkout and comparing commits + pull-requests: write # For creating/updating PR comments + jobs: build: runs-on: ubuntu-latest From 52a7f26c5e553c58ee2a8d90c369b06297d0cde7 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 22:01:02 -0500 Subject: [PATCH 15/45] split jobs --- .github/workflows/ci.yml | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64e597f..faccbb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Unit Tests +name: CI on: push: @@ -11,30 +11,53 @@ on: permissions: contents: read # For checkout and comparing commits pull-requests: write # For creating/updating PR comments - + jobs: build: runs-on: ubuntu-latest - steps: - uses: actions/checkout@v5 + - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.14' + - name: Install dependencies run: | python -m pip install --upgrade pip pip install poetry poetry install + + - name: Upload virtual environment as artifact + uses: actions/upload-artifact@v5 + with: + name: poetry-venv + path: .venv + + test: + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v5 + + - name: Download virtual environment artifact + uses: actions/download-artifact@v5 + with: + name: poetry-venv + path: .venv + - name: Run tests with coverage run: | + source .venv/bin/activate poetry run pytest ./tests --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=./src | tee pytest-coverage.txt + - name: Upload coverage reports to GitHub uses: actions/upload-artifact@v5 with: name: code-coverage-report path: pytest.xml + - name: Pytest Coverage Comment uses: MishaKav/pytest-coverage-comment@v1.2.0 with: From 3e3d44f61927a9c504d73dd57a75e80158ddb4fe Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 22:02:30 -0500 Subject: [PATCH 16/45] fix path --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index faccbb1..292b84b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: uses: actions/upload-artifact@v5 with: name: poetry-venv - path: .venv + path: ./.venv test: runs-on: ubuntu-latest @@ -45,7 +45,7 @@ jobs: uses: actions/download-artifact@v5 with: name: poetry-venv - path: .venv + path: ./.venv - name: Run tests with coverage run: | From 4038b89cfbfcf6471984b965d37e6e754c2be4a4 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 22:04:42 -0500 Subject: [PATCH 17/45] fix path --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 292b84b..d78f68a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,8 @@ jobs: uses: actions/upload-artifact@v5 with: name: poetry-venv - path: ./.venv + path: .venv/ + retention-days: 1 test: runs-on: ubuntu-latest @@ -45,7 +46,7 @@ jobs: uses: actions/download-artifact@v5 with: name: poetry-venv - path: ./.venv + path: .venv/ - name: Run tests with coverage run: | From 9cb04a1bb024ba07af0acc53c40553e6de3316e2 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 22:08:41 -0500 Subject: [PATCH 18/45] use absolute path --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d78f68a..98e0eea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: uses: actions/upload-artifact@v5 with: name: poetry-venv - path: .venv/ + path: /home/runner/work/f1-player/f1-player/.venv retention-days: 1 test: @@ -46,7 +46,7 @@ jobs: uses: actions/download-artifact@v5 with: name: poetry-venv - path: .venv/ + path: /home/runner/work/f1-player/f1-player/.venv/ - name: Run tests with coverage run: | From 760b5e583534e2fe6731c845ecbc9a8a8f03c678 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 22:13:08 -0500 Subject: [PATCH 19/45] split and hopefully fix --- .github/workflows/ci.yml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98e0eea..64f19c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,18 +22,27 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.14' + cache: "poetry" + + - name: Upgrade PIP + run: python -m pip install --upgrade pip + + - name: Install Poetry + run: pip install poetry - name: Install dependencies + run: poetry install + + - name: Verify .venv run: | - python -m pip install --upgrade pip - pip install poetry - poetry install + ls -la .venv + poetry env info - name: Upload virtual environment as artifact uses: actions/upload-artifact@v5 with: name: poetry-venv - path: /home/runner/work/f1-player/f1-player/.venv + path: .venv/ retention-days: 1 test: From 043d1078abdeec1e6f74b15832364a1d5306e0e7 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 22:16:26 -0500 Subject: [PATCH 20/45] remove cache --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64f19c0..4e5d410 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,10 +22,9 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.14' - cache: "poetry" - name: Upgrade PIP - run: python -m pip install --upgrade pip + run: python -m pip install --upgrade pip - name: Install Poetry run: pip install poetry From d4fe24fa5ad326ea252b9e81771af907e2c0c490 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 22:19:25 -0500 Subject: [PATCH 21/45] change artifact name --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e5d410..1cb91ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: - name: Upload virtual environment as artifact uses: actions/upload-artifact@v5 with: - name: poetry-venv + name: python-venv path: .venv/ retention-days: 1 @@ -53,8 +53,8 @@ jobs: - name: Download virtual environment artifact uses: actions/download-artifact@v5 with: - name: poetry-venv - path: /home/runner/work/f1-player/f1-player/.venv/ + name: python-venv + path: .venv/ - name: Run tests with coverage run: | From a1b3d25560562fc93e1c089621989c0ace2f4b8e Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 22:22:51 -0500 Subject: [PATCH 22/45] use newer upload --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1cb91ce..cc78c8f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,10 +38,12 @@ jobs: poetry env info - name: Upload virtual environment as artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: python-venv path: .venv/ + if-no-files-found: error + include-hidden-files: true retention-days: 1 test: From e82bc128375bcd6b50f9242fef6ab824c3d2d701 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 22:25:28 -0500 Subject: [PATCH 23/45] bump download artifact --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc78c8f..33f5c66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,7 @@ jobs: - uses: actions/checkout@v5 - name: Download virtual environment artifact - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v7 with: name: python-venv path: .venv/ From 62817f5a62054b2dc5296343126df5eaca9bc339 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 22:26:02 -0500 Subject: [PATCH 24/45] debug --- .github/workflows/ci.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33f5c66..b3c0fc7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,11 +32,6 @@ jobs: - name: Install dependencies run: poetry install - - name: Verify .venv - run: | - ls -la .venv - poetry env info - - name: Upload virtual environment as artifact uses: actions/upload-artifact@v6 with: @@ -58,6 +53,9 @@ jobs: name: python-venv path: .venv/ + - name: Verify .venv + run: ls -la .venv + - name: Run tests with coverage run: | source .venv/bin/activate From ec1c5bf4b2d215f82d01b5d3cd328534f9e61c1c Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 22:31:00 -0500 Subject: [PATCH 25/45] add poetry installation --- .github/workflows/ci.yml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3c0fc7..8f0134e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,26 +47,28 @@ jobs: steps: - uses: actions/checkout@v5 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.14' + + - name: Upgrade PIP + run: python -m pip install --upgrade pip + + - name: Install Poetry + run: pip install poetry + - name: Download virtual environment artifact uses: actions/download-artifact@v7 with: name: python-venv path: .venv/ - - name: Verify .venv - run: ls -la .venv - - name: Run tests with coverage run: | source .venv/bin/activate poetry run pytest ./tests --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=./src | tee pytest-coverage.txt - - name: Upload coverage reports to GitHub - uses: actions/upload-artifact@v5 - with: - name: code-coverage-report - path: pytest.xml - - name: Pytest Coverage Comment uses: MishaKav/pytest-coverage-comment@v1.2.0 with: From e1894c311a817b6eec3d1e8f1ef1546465dc2a25 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 22:36:18 -0500 Subject: [PATCH 26/45] use poetry action --- .github/workflows/ci.yml | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f0134e..4b503db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,11 +23,10 @@ jobs: with: python-version: '3.14' - - name: Upgrade PIP - run: python -m pip install --upgrade pip - - name: Install Poetry - run: pip install poetry + uses: abatilo/actions-poetry@v4 + with: + poetry-version: 2.2.1 - name: Install dependencies run: poetry install @@ -35,7 +34,7 @@ jobs: - name: Upload virtual environment as artifact uses: actions/upload-artifact@v6 with: - name: python-venv + name: poetry-venv path: .venv/ if-no-files-found: error include-hidden-files: true @@ -52,22 +51,19 @@ jobs: with: python-version: '3.14' - - name: Upgrade PIP - run: python -m pip install --upgrade pip - - name: Install Poetry - run: pip install poetry + uses: abatilo/actions-poetry@v4 + with: + poetry-version: 2.2.1 - name: Download virtual environment artifact uses: actions/download-artifact@v7 with: - name: python-venv + name: poetry-venv path: .venv/ - name: Run tests with coverage - run: | - source .venv/bin/activate - poetry run pytest ./tests --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=./src | tee pytest-coverage.txt + run: poetry run pytest ./tests --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=./src | tee pytest-coverage.txt - name: Pytest Coverage Comment uses: MishaKav/pytest-coverage-comment@v1.2.0 From 0297200584544da851a3de40adc0bb1d57d18ffc Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 22:38:56 -0500 Subject: [PATCH 27/45] install as non root --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b503db..6fcec33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: poetry-version: 2.2.1 - name: Install dependencies - run: poetry install + run: poetry install --no-root - name: Upload virtual environment as artifact uses: actions/upload-artifact@v6 From 6fe02a856cdfdffed0126b25e727cd4050a37e27 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 22:42:40 -0500 Subject: [PATCH 28/45] activate .venv --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6fcec33..48b399b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,9 @@ jobs: path: .venv/ - name: Run tests with coverage - run: poetry run pytest ./tests --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=./src | tee pytest-coverage.txt + run: | + source .venv/bin/activate + poetry run pytest ./tests --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=./src | tee pytest-coverage.txt - name: Pytest Coverage Comment uses: MishaKav/pytest-coverage-comment@v1.2.0 From 178652738f0039eab4575b9604fdf1be01a2a866 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 22:45:03 -0500 Subject: [PATCH 29/45] no need for poetry run pytest direct --- .github/workflows/ci.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48b399b..070f80e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,11 +51,6 @@ jobs: with: python-version: '3.14' - - name: Install Poetry - uses: abatilo/actions-poetry@v4 - with: - poetry-version: 2.2.1 - - name: Download virtual environment artifact uses: actions/download-artifact@v7 with: @@ -65,7 +60,7 @@ jobs: - name: Run tests with coverage run: | source .venv/bin/activate - poetry run pytest ./tests --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=./src | tee pytest-coverage.txt + pytest ./tests --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=./src | tee pytest-coverage.txt - name: Pytest Coverage Comment uses: MishaKav/pytest-coverage-comment@v1.2.0 From 72c3f4fb74c45178fe089b5ea0a7ab63a143ba81 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Wed, 14 Jan 2026 22:50:15 -0500 Subject: [PATCH 30/45] try without source --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 070f80e..524a367 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,7 @@ jobs: - name: Run tests with coverage run: | - source .venv/bin/activate + . .venv/bin/activate pytest ./tests --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=./src | tee pytest-coverage.txt - name: Pytest Coverage Comment From cda880436ff49e245a03c604d3bbd9df2d95535a Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Thu, 15 Jan 2026 20:43:32 -0500 Subject: [PATCH 31/45] chmod the venv dir --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 524a367..30c1c8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,6 +59,7 @@ jobs: - name: Run tests with coverage run: | + chmod 755 -r .venv/ . .venv/bin/activate pytest ./tests --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=./src | tee pytest-coverage.txt From 4e54cea8e347e6185ee4c8d0254ec6acc3ed59e8 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Thu, 15 Jan 2026 20:45:54 -0500 Subject: [PATCH 32/45] try again --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 30c1c8a..f639002 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,8 @@ jobs: - name: Run tests with coverage run: | - chmod 755 -r .venv/ + ls -la ./.venv + chmod -R 755 ./.venv/ . .venv/bin/activate pytest ./tests --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=./src | tee pytest-coverage.txt From 04968623c8f8c3d097b43d185bbd5c10385da3d9 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Thu, 15 Jan 2026 20:48:53 -0500 Subject: [PATCH 33/45] run via poetry --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f639002..b2edf48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,6 +51,11 @@ jobs: with: python-version: '3.14' + - name: Install Poetry + uses: abatilo/actions-poetry@v4 + with: + poetry-version: 2.2.1 + - name: Download virtual environment artifact uses: actions/download-artifact@v7 with: @@ -61,8 +66,7 @@ jobs: run: | ls -la ./.venv chmod -R 755 ./.venv/ - . .venv/bin/activate - pytest ./tests --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=./src | tee pytest-coverage.txt + poetry run pytest ./tests --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=./src | tee pytest-coverage.txt - name: Pytest Coverage Comment uses: MishaKav/pytest-coverage-comment@v1.2.0 From d6a42d919a21b362e93c52875fb8f65a50cc19ad Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Thu, 15 Jan 2026 20:51:22 -0500 Subject: [PATCH 34/45] remove test init file --- tests/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 From 306f3817c09267bfb9ed16dddfdecc863947f24a Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Thu, 15 Jan 2026 20:54:05 -0500 Subject: [PATCH 35/45] try without chmod --- .github/workflows/ci.yml | 3 +-- tests/__init__.py | 0 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 tests/__init__.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2edf48..c663473 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,8 +64,7 @@ jobs: - name: Run tests with coverage run: | - ls -la ./.venv - chmod -R 755 ./.venv/ + source ./.venv/bin/activate poetry run pytest ./tests --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=./src | tee pytest-coverage.txt - name: Pytest Coverage Comment diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 From e0200155c726875b4b9c5b8e7a2bf9953149f0d6 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Thu, 15 Jan 2026 20:56:10 -0500 Subject: [PATCH 36/45] add chmod back --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c663473..434de68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,6 +64,7 @@ jobs: - name: Run tests with coverage run: | + chmod -R 755 ./.venv/ source ./.venv/bin/activate poetry run pytest ./tests --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=./src | tee pytest-coverage.txt From 802d9bf5d2d0eb5270022c35b487888adcce0c6f Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Thu, 15 Jan 2026 21:05:32 -0500 Subject: [PATCH 37/45] try using cache --- .github/workflows/ci.yml | 43 ++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 434de68..302d0c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,16 +29,23 @@ jobs: poetry-version: 2.2.1 - name: Install dependencies - run: poetry install --no-root + run: poetry install - - name: Upload virtual environment as artifact - uses: actions/upload-artifact@v6 + - name: Cached venv + id: cached-poetry-dependencies + uses: actions/cache/save@v5 with: - name: poetry-venv - path: .venv/ - if-no-files-found: error - include-hidden-files: true - retention-days: 1 + path: .venv + key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} + +# - name: Upload virtual environment as artifact +# uses: actions/upload-artifact@v6 +# with: +# name: poetry-venv +# path: .venv/ +# if-no-files-found: error +# include-hidden-files: true +# retention-days: 1 test: runs-on: ubuntu-latest @@ -56,15 +63,25 @@ jobs: with: poetry-version: 2.2.1 - - name: Download virtual environment artifact - uses: actions/download-artifact@v7 + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache/restore@v5 with: - name: poetry-venv - path: .venv/ + path: .venv + key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} + + - name: Install dependencies + run: poetry install + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + +# - name: Download virtual environment artifact +# uses: actions/download-artifact@v7 +# with: +# name: poetry-venv +# path: .venv/ - name: Run tests with coverage run: | - chmod -R 755 ./.venv/ source ./.venv/bin/activate poetry run pytest ./tests --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=./src | tee pytest-coverage.txt From 93327dadcf91759fa43e19cdc3fa1653eec941e7 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Thu, 15 Jan 2026 21:32:04 -0500 Subject: [PATCH 38/45] linting and formatting --- .github/workflows/ci.yml | 90 +++++++++-- poetry.lock | 4 +- pyproject.toml | 9 +- src/f1p/main.py | 11 +- src/f1p/services/data_extractor/__init__.py | 152 +++++++++++------- src/f1p/services/data_extractor/enums.py | 3 +- src/f1p/ui/components/driver.py | 6 +- src/f1p/ui/components/gui/button.py | 2 +- src/f1p/ui/components/gui/drop_down.py | 62 ++++--- src/f1p/ui/components/leaderboard/__init__.py | 76 +++++---- .../leaderboard/processors/__init__.py | 3 +- src/f1p/ui/components/map.py | 6 +- src/f1p/ui/components/menu.py | 19 ++- src/f1p/ui/components/playback.py | 22 ++- src/f1p/utils/color.py | 10 +- src/f1p/utils/geometry.py | 14 +- src/f1p/utils/performance.py | 11 +- .../test_conventional_session_identifiers.py | 12 +- 18 files changed, 313 insertions(+), 199 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 302d0c8..9a5b8c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,26 +28,58 @@ jobs: with: poetry-version: 2.2.1 + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache/restore@v5 + with: + path: .venv + key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} + - name: Install dependencies run: poetry install + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - - name: Cached venv - id: cached-poetry-dependencies + - name: Cache venv + id: cache-poetry-dependencies uses: actions/cache/save@v5 + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' with: path: .venv key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} -# - name: Upload virtual environment as artifact -# uses: actions/upload-artifact@v6 -# with: -# name: poetry-venv -# path: .venv/ -# if-no-files-found: error -# include-hidden-files: true -# retention-days: 1 + lint: + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v5 - test: + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.14' + + - name: Install Poetry + uses: abatilo/actions-poetry@v4 + with: + poetry-version: 2.2.1 + + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache/restore@v5 + with: + path: .venv + key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} + + - name: Install dependencies + run: poetry install + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + + - name: Run linter + run: | + source ./.venv/bin/activate + poetry run ruff check --config=pyproject.toml + + format: runs-on: ubuntu-latest needs: build steps: @@ -74,11 +106,37 @@ jobs: run: poetry install if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' -# - name: Download virtual environment artifact -# uses: actions/download-artifact@v7 -# with: -# name: poetry-venv -# path: .venv/ + - name: Run linter + run: | + source ./.venv/bin/activate + poetry run ruff format --check --config=pyproject.toml + + test: + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v5 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.14' + + - name: Install Poetry + uses: abatilo/actions-poetry@v4 + with: + poetry-version: 2.2.1 + + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache/restore@v5 + with: + path: .venv + key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} + + - name: Install dependencies + run: poetry install + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - name: Run tests with coverage run: | diff --git a/poetry.lock b/poetry.lock index c7b8b5f..eceaacf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2171,5 +2171,5 @@ files = [ [metadata] lock-version = "2.1" -python-versions = ">=3.13,<4.0" -content-hash = "125fb2dfa66bd455fd7e51ceba40cc1afa28e7b2de656779591755a7ef41360f" +python-versions = ">=3.14,<4.0" +content-hash = "f1a584d4bf60b74aabbe2d24b1829ebe80f5c9f6d74235477fa5e560a18f469d" diff --git a/pyproject.toml b/pyproject.toml index 59256e7..1854002 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "F1 Player" -version = "0.0.1" +version = "0.0.4" description = "A tool for watching F1 races from a data perspective." authors = [ "Mitko Toshev " @@ -9,7 +9,7 @@ readme = "README.md" packages = [{include = "f1p", from = "src"}] [tool.poetry.dependencies] -python = ">=3.13,<4.0" +python = ">=3.14,<4.0" fastf1 = "^3.7.0" panda3d = "^1.10.15" pandas-stubs = "^2.3.3.251219" @@ -34,10 +34,13 @@ build-backend = "poetry.core.masonry.api" f1p = "f1p.main:app" [tool.ruff] -target-version = "py313" +target-version = "py314" line-length = 120 src = ["src", "tests"] preview = true +exclude = [ + "src/f1p/services/procedural3d" +] [tool.ruff.lint] select = ["E", "F", "I", "W", "ARG", "A", "COM", "C4", "PIE", "T20", "PT"] diff --git a/src/f1p/main.py b/src/f1p/main.py index b6deffe..9a220f5 100644 --- a/src/f1p/main.py +++ b/src/f1p/main.py @@ -66,7 +66,7 @@ def register_ui_components(self) -> Self: 30, self.symbols_font, self.text_font, - self.data_extractor + self.data_extractor, ) circuit_map = Map(self.render, self.data_extractor) @@ -76,7 +76,7 @@ def register_ui_components(self) -> Self: self.symbols_font, self.text_font, circuit_map, - self.data_extractor + self.data_extractor, ) self.ui_components = [ @@ -90,9 +90,4 @@ def register_ui_components(self) -> Self: app = F1PlayerApp() app.disableMouse() # disable camera controls -( - app.configure_window() - .draw_menu() - .register_ui_components() - .run() -) +(app.configure_window().draw_menu().register_ui_components().run()) diff --git a/src/f1p/services/data_extractor/__init__.py b/src/f1p/services/data_extractor/__init__.py index 1ed1a96..3471b5a 100644 --- a/src/f1p/services/data_extractor/__init__.py +++ b/src/f1p/services/data_extractor/__init__.py @@ -5,20 +5,20 @@ import fastf1 import pandas as pd from direct.showbase.MessengerGlobal import messenger -from fastf1.core import Session, Lap, Laps, Telemetry -from fastf1.events import EventSchedule, Event +from fastf1.core import Lap, Laps, Session, Telemetry +from fastf1.events import Event, EventSchedule from fastf1.mvapi import CircuitInfo -from panda3d.core import deg2Rad, LVecBase4f -from pandas import DataFrame, Timedelta, Series +from panda3d.core import LVecBase4f, deg2Rad +from pandas import DataFrame, Series, Timedelta -from f1p.utils.geometry import find_center, resize_pos_data, center_pos_data +from f1p.utils.geometry import center_pos_data, find_center, resize_pos_data class DataExtractorService: year: int event_name: str session_id: str - cache_path: Path = Path(__file__).parent.parent.parent.parent.parent / '.fastf1-cache' + cache_path: Path = Path(__file__).parent.parent.parent.parent.parent / ".fastf1-cache" def __init__(self): self._event_schedule: EventSchedule | None = None @@ -151,8 +151,8 @@ def track_status_colors(self) -> DataFrame: LVecBase4f(1, 1, 1, 0.8), LVecBase4f(0, 0, 0, 0.8), LVecBase4f(0, 0, 0, 0.8), - ] - } + ], + }, ) return self._track_status_colors @@ -218,7 +218,7 @@ def process_track_statuses(self, width: int) -> None: pixel_per_tick = width / self.session_ticks - df.loc[:,"Pixel"] = df.loc[:,"SessionTimeTick"] * pixel_per_tick + df.loc[:, "Pixel"] = df.loc[:, "SessionTimeTick"] * pixel_per_tick ts_df = self.track_status.copy() ts_df = ts_df[ts_df["Time"] >= self.session_start_time] @@ -228,16 +228,22 @@ def process_track_statuses(self, width: int) -> None: for record in ts_df.itertuples(): ts_df.loc[ts_df["Time"] == record.Time, "SessionTimeTick"] = df.loc[ - df["SessionTime"] <= record.Time, "SessionTimeTick"].max() + df["SessionTime"] <= record.Time, + "SessionTimeTick", + ].max() ts_df.loc[ts_df["Time"] == record.Time, "SessionTimeTickEnd"] = df.loc[ - df["SessionTime"] <= record.EndTime, "SessionTimeTick"].max() + df["SessionTime"] <= record.EndTime, + "SessionTimeTick", + ].max() ts_df["SessionTimeTick"] = ts_df["SessionTimeTick"].astype("int64") ts_df["SessionTimeTickEnd"] = ts_df["SessionTimeTickEnd"].astype("int64") ts_df = ts_df.merge(df, on="SessionTimeTick", how="left") ts_df = ts_df.rename(columns={"Pixel": "PixelStart"}).drop(columns="SessionTime") - ts_df = ts_df.merge(df, left_on="SessionTimeTickEnd", right_on="SessionTimeTick", how="left").rename(columns={"SessionTimeTick_x": "SessionTimeTick"}) + ts_df = ts_df.merge(df, left_on="SessionTimeTickEnd", right_on="SessionTimeTick", how="left").rename( + columns={"SessionTimeTick_x": "SessionTimeTick"}, + ) ts_df = ts_df.rename(columns={"Pixel": "PixelEnd"}).drop(columns=["SessionTime", "SessionTimeTick_y"]) ts_df = ts_df.drop(columns=["Time", "EndTime"]).reset_index() @@ -286,7 +292,8 @@ def combine_position_data(self) -> Self: def remove_records_before_session_start_time(self) -> Self: self.processed_pos_data = self.processed_pos_data[ - self.processed_pos_data["SessionTime"] >= self.session_start_time] + self.processed_pos_data["SessionTime"] >= self.session_start_time + ] return self @@ -318,24 +325,31 @@ def process_laps(self) -> Self: laps = self.laps.copy() laps.loc[laps["Sector1SessionTime"].isna(), "Sector1SessionTime"] = ( - laps.loc[laps["Sector1SessionTime"].isna(), "LapStartTime"] + laps.loc[laps["Sector1SessionTime"].isna(), "Sector1Time"] + laps.loc[laps["Sector1SessionTime"].isna(), "LapStartTime"] + + laps.loc[laps["Sector1SessionTime"].isna(), "Sector1Time"] ) laps.loc[laps["Sector2SessionTime"].isna(), "Sector2SessionTime"] = ( - laps.loc[laps["Sector2SessionTime"].isna(), "Sector1SessionTime"] + laps.loc[ - laps["Sector2SessionTime"].isna(), "Sector2Time"] + laps.loc[laps["Sector2SessionTime"].isna(), "Sector1SessionTime"] + + laps.loc[laps["Sector2SessionTime"].isna(), "Sector2Time"] ) laps.loc[laps["Sector3SessionTime"].isna(), "Sector3SessionTime"] = ( - laps.loc[laps["Sector3SessionTime"].isna(), "Sector2SessionTime"] + laps.loc[ - laps["Sector3SessionTime"].isna(), "Sector3Time"] + laps.loc[laps["Sector3SessionTime"].isna(), "Sector2SessionTime"] + + laps.loc[laps["Sector3SessionTime"].isna(), "Sector3Time"] ) lap_start_time_in_milliseconds = laps["LapStartTime"].fillna(Timedelta(milliseconds=0)).dt.total_seconds() * 1e3 laps["LapStartTimeMilliseconds"] = lap_start_time_in_milliseconds.astype("int64") - sector1_session_time_in_milliseconds = laps["Sector1SessionTime"].fillna(Timedelta(milliseconds=0)).dt.total_seconds() * 1e3 + sector1_session_time_in_milliseconds = ( + laps["Sector1SessionTime"].fillna(Timedelta(milliseconds=0)).dt.total_seconds() * 1e3 + ) laps["Sector1SessionTimeMilliseconds"] = sector1_session_time_in_milliseconds.astype("int64") - sector2_session_time_in_milliseconds = laps["Sector2SessionTime"].fillna(Timedelta(milliseconds=0)).dt.total_seconds() * 1e3 + sector2_session_time_in_milliseconds = ( + laps["Sector2SessionTime"].fillna(Timedelta(milliseconds=0)).dt.total_seconds() * 1e3 + ) laps["Sector2SessionTimeMilliseconds"] = sector2_session_time_in_milliseconds.astype("int64") - sector3_session_time_in_milliseconds = laps["Sector3SessionTime"].fillna(Timedelta(milliseconds=0)).dt.total_seconds() * 1e3 + sector3_session_time_in_milliseconds = ( + laps["Sector3SessionTime"].fillna(Timedelta(milliseconds=0)).dt.total_seconds() * 1e3 + ) laps["Sector3SessionTimeMilliseconds"] = sector3_session_time_in_milliseconds.astype("int64") lap_time_in_milliseconds = laps["LapTime"].fillna(Timedelta(milliseconds=1)).dt.total_seconds() * 1e3 @@ -348,9 +362,21 @@ def process_laps(self) -> Self: pit_out_time_in_milliseconds = laps.loc[laps["PitOutTime"].notna(), "PitOutTime"].dt.total_seconds() * 1e3 laps.loc[laps["PitOutTime"].notna(), "PitOutTimeMilliseconds"] = pit_out_time_in_milliseconds.astype("int64") - laps["S1DiffToCarAhead"] = laps.sort_values(by=["Sector1SessionTimeMilliseconds"], ascending=[True]).groupby("LapNumber")["Sector1SessionTimeMilliseconds"].diff() - laps["S2DiffToCarAhead"] = laps.sort_values(by=["Sector2SessionTimeMilliseconds"], ascending=[True]).groupby("LapNumber")["Sector2SessionTimeMilliseconds"].diff() - laps["S3DiffToCarAhead"] = laps.sort_values(by=["Sector3SessionTimeMilliseconds"], ascending=[True]).groupby("LapNumber")["Sector3SessionTimeMilliseconds"].diff() + laps["S1DiffToCarAhead"] = ( + laps.sort_values(by=["Sector1SessionTimeMilliseconds"], ascending=[True]) + .groupby("LapNumber")["Sector1SessionTimeMilliseconds"] + .diff() + ) + laps["S2DiffToCarAhead"] = ( + laps.sort_values(by=["Sector2SessionTimeMilliseconds"], ascending=[True]) + .groupby("LapNumber")["Sector2SessionTimeMilliseconds"] + .diff() + ) + laps["S3DiffToCarAhead"] = ( + laps.sort_values(by=["Sector3SessionTimeMilliseconds"], ascending=[True]) + .groupby("LapNumber")["Sector3SessionTimeMilliseconds"] + .diff() + ) laps["LastLapTimeMilliseconds"] = laps.groupby("DriverNumber")["LapTimeMilliseconds"].shift(1) laps["FastestLapTimeMillisecondsSoFar"] = laps.groupby("DriverNumber")["LastLapTimeMilliseconds"].cummin() @@ -367,32 +393,43 @@ def merge_pos_and_laps(self) -> Self: for lap in laps_df.itertuples(): pos_data_df.loc[ - (pos_data_df["DriverNumber"] == lap.DriverNumber) & - (pos_data_df["SessionTimeMilliseconds"] >= lap.LapStartTimeMilliseconds) & - (pos_data_df["SessionTimeMilliseconds"] < lap.LapEndTimeMilliseconds), - "LapNumber" + (pos_data_df["DriverNumber"] == lap.DriverNumber) + & (pos_data_df["SessionTimeMilliseconds"] >= lap.LapStartTimeMilliseconds) + & (pos_data_df["SessionTimeMilliseconds"] < lap.LapEndTimeMilliseconds), + "LapNumber", ] = lap.LapNumber - combined_df = ( - pos_data_df.merge(laps_df, on=["DriverNumber", "LapNumber"], how="left") - .rename(columns={"Time_x": "Time", "Time_y": "Time_Lap"}) + combined_df = pos_data_df.merge(laps_df, on=["DriverNumber", "LapNumber"], how="left").rename( + columns={"Time_x": "Time", "Time_y": "Time_Lap"}, ) combined_df["LapNumber"] = combined_df.groupby("DriverNumber")["LapNumber"].ffill() - combined_df["LapStartTimeMilliseconds"] = combined_df.groupby("DriverNumber")["LapStartTimeMilliseconds"].ffill() + combined_df["LapStartTimeMilliseconds"] = combined_df.groupby("DriverNumber")[ + "LapStartTimeMilliseconds" + ].ffill() combined_df["LapEndTimeMilliseconds"] = combined_df.groupby("DriverNumber")["LapEndTimeMilliseconds"].ffill() combined_df.loc[ - (combined_df["LapNumber"] == self.total_laps) & - (combined_df["SessionTimeMilliseconds"] > combined_df["LapEndTimeMilliseconds"]), - "LapNumber" + (combined_df["LapNumber"] == self.total_laps) + & (combined_df["SessionTimeMilliseconds"] > combined_df["LapEndTimeMilliseconds"]), + "LapNumber", ] = self.total_laps + 1 - combined_df["ElapsedTimeSinceStartOfLapMilliseconds"] = combined_df["SessionTimeMilliseconds"] - combined_df["LapStartTimeMilliseconds"] - combined_df["LapPercentageCompletion"] = combined_df["ElapsedTimeSinceStartOfLapMilliseconds"] / combined_df["LapTimeMilliseconds"] + combined_df["ElapsedTimeSinceStartOfLapMilliseconds"] = ( + combined_df["SessionTimeMilliseconds"] - combined_df["LapStartTimeMilliseconds"] + ) + combined_df["LapPercentageCompletion"] = ( + combined_df["ElapsedTimeSinceStartOfLapMilliseconds"] / combined_df["LapTimeMilliseconds"] + ) combined_df["LapPercentageCompletion"] = combined_df["LapPercentageCompletion"].fillna(0) combined_df["LapsCompletion"] = (combined_df["LapNumber"] - 1) + combined_df["LapPercentageCompletion"] - combined_df["PositionIndex"] = combined_df.sort_values(by=["SessionTimeTick", "LapsCompletion"], ascending=[True, False]).groupby("SessionTimeTick").cumcount().add(1) - 1 + combined_df["PositionIndex"] = ( + combined_df.sort_values(by=["SessionTimeTick", "LapsCompletion"], ascending=[True, False]) + .groupby("SessionTimeTick") + .cumcount() + .add(1) + - 1 + ) combined_df.loc[combined_df["Position"].notna(), "IsDNF"] = False combined_df.loc[combined_df["Position"].isna(), "IsDNF"] = True @@ -404,31 +441,41 @@ def merge_pos_and_laps(self) -> Self: combined_df.loc[combined_df["SessionTimeMilliseconds"] >= end_of_race, "PositionIndex"] = pd.NA combined_df["PositionIndex"] = combined_df.groupby("DriverNumber")["PositionIndex"].ffill().astype("int64") - combined_df["FastestLapTimeMilliseconds"] = combined_df.sort_values(by=["SessionTimeTick", "LapsCompletion"], ascending=[True, False])["FastestLapTimeMillisecondsSoFar"].cummin() - combined_df.loc[combined_df["FastestLapTimeMillisecondsSoFar"] == combined_df["FastestLapTimeMilliseconds"], "HasFastestLap"] = True + combined_df["FastestLapTimeMilliseconds"] = combined_df.sort_values( + by=["SessionTimeTick", "LapsCompletion"], + ascending=[True, False], + )["FastestLapTimeMillisecondsSoFar"].cummin() + combined_df.loc[ + combined_df["FastestLapTimeMillisecondsSoFar"] == combined_df["FastestLapTimeMilliseconds"], + "HasFastestLap", + ] = True combined_df.loc[combined_df["HasFastestLap"].isna(), "HasFastestLap"] = False combined_df["HasFastestLap"] = combined_df.groupby("DriverNumber")["HasFastestLap"].ffill() combined_df.loc[ (combined_df["SessionTimeMilliseconds"] >= combined_df["Sector1SessionTimeMilliseconds"]), - "DiffToCarInFront" + "DiffToCarInFront", ] = combined_df["S1DiffToCarAhead"] combined_df.loc[ (combined_df["SessionTimeMilliseconds"] >= combined_df["Sector2SessionTimeMilliseconds"]), - "DiffToCarInFront" + "DiffToCarInFront", ] = combined_df["S2DiffToCarAhead"] combined_df.loc[ (combined_df["SessionTimeMilliseconds"] >= combined_df["Sector3SessionTimeMilliseconds"]), - "DiffToCarInFront" + "DiffToCarInFront", ] = combined_df["S3DiffToCarAhead"] combined_df.loc[ (combined_df["PositionIndex"] == 0), - "DiffToCarInFront" + "DiffToCarInFront", ] = 0 combined_df["DiffToCarInFront"] = combined_df.groupby("DriverNumber")["DiffToCarInFront"].ffill() combined_df["DiffToCarInFront"] = round(combined_df["DiffToCarInFront"] / 1000, 3) - combined_df["DiffToLeader"] = combined_df .sort_values(by=["SessionTimeTick", "LapsCompletion"], ascending=[True, False]).groupby(["SessionTimeTick"])["DiffToCarInFront"].cumsum() + combined_df["DiffToLeader"] = ( + combined_df.sort_values(by=["SessionTimeTick", "LapsCompletion"], ascending=[True, False]) + .groupby(["SessionTimeTick"])["DiffToCarInFront"] + .cumsum() + ) combined_df["DiffToLeader"] = round(combined_df["DiffToLeader"], 3) self.processed_pos_data = combined_df @@ -440,16 +487,13 @@ def add_in_pit_column(self) -> Self: df.loc[ ( - ( - df["PitInTimeMilliseconds"].notna() & - (df["PitInTimeMilliseconds"] <= df["SessionTimeMilliseconds"]) - ) | - ( - df["PitOutTimeMilliseconds"].notna() & - (df["PitOutTimeMilliseconds"] >= df["SessionTimeMilliseconds"]) + (df["PitInTimeMilliseconds"].notna() & (df["PitInTimeMilliseconds"] <= df["SessionTimeMilliseconds"])) + | ( + df["PitOutTimeMilliseconds"].notna() + & (df["PitOutTimeMilliseconds"] >= df["SessionTimeMilliseconds"]) ) ), - "InPit" + "InPit", ] = True df["InPit"] = df["InPit"].astype("boolean").fillna(False) @@ -476,7 +520,7 @@ def process_tire_compound_columns(self) -> Self: df.loc[df["Compound"] == "W", "CompoundColor"] = df.loc[df["Compound"] == "W", "WCompoundColor"] self.processed_pos_data = df.drop( - columns=["SCompoundColor", "MCompoundColor", "HCompoundColor", "ICompoundColor", "WCompoundColor"] + columns=["SCompoundColor", "MCompoundColor", "HCompoundColor", "ICompoundColor", "WCompoundColor"], ) return self diff --git a/src/f1p/services/data_extractor/enums.py b/src/f1p/services/data_extractor/enums.py index c331053..1193792 100644 --- a/src/f1p/services/data_extractor/enums.py +++ b/src/f1p/services/data_extractor/enums.py @@ -12,6 +12,7 @@ class ConventionalSessionIdentifiers(Enum): def all_values() -> list[str]: return [member.value for member in ConventionalSessionIdentifiers] + class SprintQualifyingSessionIdentifiers(Enum): FREE_PRACTICE_1 = "Practice 1" SPRINT_QUALIFYING = "Sprint Qualifying" @@ -21,4 +22,4 @@ class SprintQualifyingSessionIdentifiers(Enum): @staticmethod def all_values() -> list[str]: - return [member.value for member in SprintQualifyingSessionIdentifiers] \ No newline at end of file + return [member.value for member in SprintQualifyingSessionIdentifiers] diff --git a/src/f1p/ui/components/driver.py b/src/f1p/ui/components/driver.py index b1a9e02..d097cbe 100644 --- a/src/f1p/ui/components/driver.py +++ b/src/f1p/ui/components/driver.py @@ -1,8 +1,8 @@ from decimal import Decimal from direct.showbase.DirectObject import DirectObject -from fastf1.core import Lap, Telemetry, Laps -from panda3d.core import NodePath, LVecBase4f +from fastf1.core import Lap, Laps, Telemetry +from panda3d.core import LVecBase4f, NodePath from pandas import Series, Timedelta from f1p.services.procedural3d import SphereMaker @@ -37,7 +37,7 @@ def __init__( self.team_name = team_name self.team_color = team_color self.pos_data = pos_data - self.ticks = self.pos_data.set_index("SessionTimeTick").to_dict(orient='index') + self.ticks = self.pos_data.set_index("SessionTimeTick").to_dict(orient="index") self.current_lap = current_lap self.current_lap_time = current_lap_time self.time_to_car_in_front = time_to_car_in_front diff --git a/src/f1p/ui/components/gui/button.py b/src/f1p/ui/components/gui/button.py index 964702d..26b0ef4 100644 --- a/src/f1p/ui/components/gui/button.py +++ b/src/f1p/ui/components/gui/button.py @@ -15,4 +15,4 @@ def __init__(self, parent=None, **kwargs): DirectButton.__init__(self, parent, **kwargs) - self.initialiseoptions(BlackButton) \ No newline at end of file + self.initialiseoptions(BlackButton) diff --git a/src/f1p/ui/components/gui/drop_down.py b/src/f1p/ui/components/gui/drop_down.py index ec8a212..0193ec9 100644 --- a/src/f1p/ui/components/gui/drop_down.py +++ b/src/f1p/ui/components/gui/drop_down.py @@ -1,7 +1,7 @@ from direct.gui.DirectFrame import DirectFrame -from direct.gui.DirectGuiGlobals import RAISED, B1RELEASE, WITHIN, WITHOUT +from direct.gui.DirectGuiGlobals import B1RELEASE, RAISED, WITHIN, WITHOUT from direct.gui.DirectOptionMenu import DirectOptionMenu -from panda3d.core import StaticTextFont, NodePath, TextNode +from panda3d.core import NodePath, StaticTextFont, TextNode from f1p.ui.components.gui.button import BlackButton @@ -15,7 +15,6 @@ def __init__( font: StaticTextFont | None = None, font_scale: float = 1.0, popup_menu_below: bool = True, - scale: float = 1.0, item_scale: float = 1.0, frameColor: tuple[float, float, float, float] = (0.15, 0.15, 0.15, 1), @@ -25,9 +24,7 @@ def __init__( pressEffect: int = 1, text_fg: tuple[float, float, float, float] = (1, 1, 1, 1), text_pos: tuple[float, float] = (0, 0), - items: list[str] | None = None, - **kwargs, ): self.width = width @@ -94,11 +91,11 @@ def setItems(self): Create new popup menu to reflect specified set of items """ # Remove old component if it exits - if self.popupMenu != None: - self.destroycomponent('popupMenu') + if self.popupMenu is not None: + self.destroycomponent("popupMenu") # Create new component self.popupMenu = self.createcomponent( - 'popupMenu', + "popupMenu", (), None, DirectFrame, @@ -107,19 +104,19 @@ def setItems(self): relief=RAISED, ) # Make sure it is on top of all the other gui widgets - self.popupMenu.setBin('gui-popup', 0) + self.popupMenu.setBin("gui-popup", 0) self.highlightedIndex = None - if not self['items']: + if not self["items"]: return # Create a new component for each item # Find the maximum extents of all items itemIndex = 0 self.minX = self.maxX = self.minZ = self.maxZ = None - for item in self['items']: + for item in self["items"]: c = self.createcomponent( - 'item%d' % itemIndex, + "item%d" % itemIndex, (), - 'item', + "item", BlackButton, (self.popupMenu,), text=item, @@ -133,7 +130,7 @@ def setItems(self): relief=RAISED, borderWidth=self.item_border_width, pressEffect=self.item_press_effect, - command=lambda i=itemIndex: self.set(i) + command=lambda i=itemIndex: self.set(i), ) bounds = c.getBounds() @@ -159,32 +156,31 @@ def setItems(self): self.maxHeight = self.maxZ - self.minZ # Adjust frame size for each item and bind actions to mouse events for i in range(itemIndex): - item = self.component('item%d' % i) + item = self.component("item%d" % i) # So entire extent of item's slot on popup is reactive to mouse - item['frameSize'] = self.item_frame_size + item["frameSize"] = self.item_frame_size # Move it to its correct position on the popup item.setPos(0, 0, (i * (-self.height * self.item_scale))) item.bind(B1RELEASE, self.hidePopupMenu) # Highlight background when mouse is in item - item.bind(WITHIN, lambda x, i=i, item=item: self._highlightItem(item, i)) + item.bind(WITHIN, lambda _, index=i, the_item=item: self._highlightItem(the_item, index)) # Restore specified color upon exiting - fc = item['frameColor'] - item.bind(WITHOUT, lambda x, item=item, fc=fc: self._unhighlightItem(item, fc)) + fc = item["frameColor"] + item.bind(WITHOUT, lambda _, the_item=item, frame_color=fc: self._unhighlightItem(the_item, frame_color)) # Set popup menu frame size to encompass all items - f = self.component('popupMenu') - f['frameSize'] = (0, self.width, -self.maxHeight * itemIndex, 0) + f = self.component("popupMenu") + f["frameSize"] = (0, self.width, -self.maxHeight * itemIndex, 0) # Determine what initial item to display and set text accordingly - if self['initialitem']: - self.set(self['initialitem'], fCommand=0) + if self["initialitem"]: + self.set(self["initialitem"], fCommand=0) else: # No initial item specified, just use first item self.set(0, fCommand=0) # Position popup Marker to the right of the button pm = self.popupMarker - pmw = (pm.getWidth() * pm.getScale()[0] + - 2 * self['popupMarkerBorder'][0]) + pmw = pm.getWidth() * pm.getScale()[0] + 2 * self["popupMarkerBorder"][0] if self.initFrameSize: # Use specified frame size bounds = list(self.initFrameSize) @@ -201,7 +197,7 @@ def setItems(self): # Adjust popup menu button to fit all items (or use user specified # frame size bounds[1] += pmw - self['frameSize'] = (bounds[0], bounds[1], bounds[2], bounds[3]) + self["frameSize"] = (bounds[0], bounds[1], bounds[2], bounds[3]) # Set initial state self.hidePopupMenu() @@ -216,14 +212,14 @@ def showPopupMenu(self, event=None): self.popupMenu.setZ(z_coordinate) def _highlightItem(self, item, index): - self._prevItemTextScale = item['text_scale'] - item['frameColor'] = self.highlight_color - item['frameSize'] = self.item_frame_size - item['text_scale'] = (self.item_text_scale, self.item_text_scale) # OVERRODE THE INSANITY THAT WAS THIS LINE + self._prevItemTextScale = item["text_scale"] + item["frameColor"] = self.highlight_color + item["frameSize"] = self.item_frame_size + item["text_scale"] = (self.item_text_scale, self.item_text_scale) # OVERRODE THE INSANITY THAT WAS THIS LINE self.highlightedIndex = index def _unhighlightItem(self, item, frameColor): - item['frameColor'] = frameColor - item['frameSize'] = self.item_frame_size - item['text_scale'] = self._prevItemTextScale + item["frameColor"] = frameColor + item["frameSize"] = self.item_frame_size + item["text_scale"] = self._prevItemTextScale self.highlightedIndex = None diff --git a/src/f1p/ui/components/leaderboard/__init__.py b/src/f1p/ui/components/leaderboard/__init__.py index 6d24654..3079d00 100644 --- a/src/f1p/ui/components/leaderboard/__init__.py +++ b/src/f1p/ui/components/leaderboard/__init__.py @@ -3,13 +3,17 @@ from direct.gui.OnscreenText import OnscreenText from direct.showbase.DirectObject import DirectObject from fastf1.core import Laps -from panda3d.core import StaticTextFont, Point3, TransparencyAttrib, TextNode +from panda3d.core import Point3, StaticTextFont, TextNode, TransparencyAttrib from f1p.services.data_extractor import DataExtractorService from f1p.ui.components.driver import Driver from f1p.ui.components.gui.drop_down import BlackDropDown -from f1p.ui.components.leaderboard.processors import LeaderboardProcessor, IntervalLeaderboardProcessor, \ - TiresLeaderboardProcessor, LeaderLeaderboardProcessor +from f1p.ui.components.leaderboard.processors import ( + IntervalLeaderboardProcessor, + LeaderboardProcessor, + LeaderLeaderboardProcessor, + TiresLeaderboardProcessor, +) from f1p.ui.components.map import Map @@ -20,7 +24,7 @@ def __init__( symbols_font: StaticTextFont, text_font: StaticTextFont, circuit_map: Map, - data_extractor: DataExtractorService + data_extractor: DataExtractorService, ): super().__init__() @@ -75,7 +79,7 @@ def render_frame(self) -> None: parent=self.pixel2d, frameColor=(0.20, 0.20, 0.20, 0.7), frameSize=(0, self.width, 0, -self.height), - pos=Point3(20, 0, -50) + pos=Point3(20, 0, -50), ) def render_track_status_frame(self) -> None: @@ -83,19 +87,19 @@ def render_track_status_frame(self) -> None: parent=self.frame, frameColor=self.data_extractor.green_flag_track_status_color, frameSize=(0, self.width - 5, 0, -1), - pos=Point3(2, 0, -2) + pos=Point3(2, 0, -2), ) self.track_status_frame_left = DirectFrame( parent=self.frame, frameColor=self.data_extractor.green_flag_track_status_color, frameSize=(0, 1, 0, self.width - 5), - pos=Point3(2, 0, -self.width + 3) + pos=Point3(2, 0, -self.width + 3), ) def render_f1_logo(self) -> None: self.f1_logo = OnscreenImage( - image='./src/f1p/ui/images/f1_logo.png', + image="./src/f1p/ui/images/f1_logo.png", pos=Point3(self.width / 2, 0, -27), scale=self.width / 4, parent=self.frame, @@ -107,7 +111,7 @@ def render_lap_counter(self) -> None: parent=self.frame, frameColor=(0.1, 0.1, 0.1, 0.7), frameSize=(0, self.width, 0, 40), - pos=Point3(0, 0, -90) + pos=Point3(0, 0, -90), ) self.lap_counter = OnscreenText( @@ -116,7 +120,7 @@ def render_lap_counter(self) -> None: scale=self.width / 10, fg=(1, 1, 1, 0.8), font=self.text_font, - text=f'LAP 1/{self.total_laps}', + text=f"LAP 1/{self.total_laps}", ) def render_track_status(self) -> None: @@ -124,7 +128,7 @@ def render_track_status(self) -> None: parent=self.frame, frameColor=self.data_extractor.green_flag_track_status_color, frameSize=(0, self.width - 2, 0, 30), - pos=Point3(2, 0, -120) + pos=Point3(2, 0, -120), ) self.track_status = OnscreenText( @@ -146,23 +150,25 @@ def switch_mode(self, mode: str) -> None: self.mode = "tires" def render_mode_selector(self) -> None: - BlackDropDown( - parent=self.frame, - width=40, - height=30, - font=self.symbols_font, - font_scale=20, - popup_menu_below=True, - command=self.switch_mode, - text="leaderboard", - text_pos=(20, -5), - text_align=TextNode.ACenter, - item_text_align=TextNode.ACenter, - items=["🕒", "🕘", "⛁"], - item_scale=1.0, - initialitem=0, - pos=Point3(self.width - 45, 0, -22), - ), + ( + BlackDropDown( + parent=self.frame, + width=40, + height=30, + font=self.symbols_font, + font_scale=20, + popup_menu_below=True, + command=self.switch_mode, + text="leaderboard", + text_pos=(20, -5), + text_align=TextNode.ACenter, + item_text_align=TextNode.ACenter, + items=["🕒", "🕘", "⛁"], + item_scale=1.0, + initialitem=0, + pos=Point3(self.width - 45, 0, -22), + ), + ) def render_drivers(self) -> None: offset_from_top = 140 @@ -176,7 +182,7 @@ def render_drivers(self) -> None: fg=(1, 1, 1, 0.8), font=self.symbols_font, text="", - ) + ), ) OnscreenText( @@ -193,8 +199,8 @@ def render_drivers(self) -> None: parent=self.frame, frameColor=driver.team_color_obj, frameSize=(0, 12, 0, 12), - pos=Point3(40, 0, -offset_from_top - 2 - (index * 23)) - ) + pos=Point3(40, 0, -offset_from_top - 2 - (index * 23)), + ), ) self.driver_abbreviations.append( @@ -205,18 +211,18 @@ def render_drivers(self) -> None: fg=(1, 1, 1, 0.8), font=self.text_font, text=driver.abbreviation, - ) + ), ) self.driver_times.append( - OnscreenText( + OnscreenText( parent=self.frame, pos=(145, -offset_from_top - (index * 23)), scale=self.width / 14, fg=(1, 1, 1, 0.8), font=self.text_font, text="NO TIME", - ) + ), ) self.driver_tires.append( @@ -227,7 +233,7 @@ def render_drivers(self) -> None: fg=(1, 0, 0, 0.8), font=self.text_font, text="S", - ) + ), ) has_fastest_lap = OnscreenText( diff --git a/src/f1p/ui/components/leaderboard/processors/__init__.py b/src/f1p/ui/components/leaderboard/processors/__init__.py index f842c3c..3c1bb5f 100644 --- a/src/f1p/ui/components/leaderboard/processors/__init__.py +++ b/src/f1p/ui/components/leaderboard/processors/__init__.py @@ -38,8 +38,7 @@ def __init__( self.has_fastest_lap = has_fastest_lap self.data_extractor = data_extractor - def update_driver(self, driver: Driver, current_record: Series, index: int) -> None: - ... + def update_driver(self, driver: Driver, current_record: Series, index: int) -> None: ... def update(self, session_time_tick: int) -> None: total_laps = self.data_extractor.total_laps diff --git a/src/f1p/ui/components/map.py b/src/f1p/ui/components/map.py index 90b93fa..61a44b7 100644 --- a/src/f1p/ui/components/map.py +++ b/src/f1p/ui/components/map.py @@ -34,7 +34,7 @@ def render_map(self, df: DataFrame) -> None: dx = np.gradient(track_x) dy = np.gradient(track_y) - norm = np.sqrt(dx ** 2 + dy ** 2) + norm = np.sqrt(dx**2 + dy**2) norm[norm == 0] = 10 dx /= norm dy /= norm @@ -49,14 +49,14 @@ def render_map(self, df: DataFrame) -> None: inner_df = new_df.copy() inner_df["X"] = x_inner inner_df["Y"] = y_inner - inner_track = inner_df.loc[:, ('X', 'Y', 'Z')].to_numpy() + inner_track = inner_df.loc[:, ("X", "Y", "Z")].to_numpy() self.inner_border_node_path = self.draw_track(inner_track, (0.9, 0.9, 0.9, 1)) self.inner_border_node_path.reparentTo(self.parent) outer_df = new_df.copy() outer_df["X"] = x_outer outer_df["Y"] = y_outer - outer_track = outer_df.loc[:, ('X', 'Y', 'Z')].to_numpy() + outer_track = outer_df.loc[:, ("X", "Y", "Z")].to_numpy() self.outer_border_node_path = self.draw_track(outer_track, (0.9, 0.9, 0.9, 1)) self.outer_border_node_path.reparentTo(self.parent) diff --git a/src/f1p/ui/components/menu.py b/src/f1p/ui/components/menu.py index b984744..8b31a16 100644 --- a/src/f1p/ui/components/menu.py +++ b/src/f1p/ui/components/menu.py @@ -6,12 +6,19 @@ from panda3d.core import Point3, StaticTextFont from f1p.services.data_extractor import DataExtractorService -from f1p.services.data_extractor.enums import SprintQualifyingSessionIdentifiers, ConventionalSessionIdentifiers +from f1p.services.data_extractor.enums import ConventionalSessionIdentifiers, SprintQualifyingSessionIdentifiers from f1p.ui.components.gui.drop_down import BlackDropDown class Menu: - def __init__(self, pixel2d, width: int, height: int, text_font: StaticTextFont, data_extractor: DataExtractorService): + def __init__( + self, + pixel2d, + width: int, + height: int, + text_font: StaticTextFont, + data_extractor: DataExtractorService, + ): self.pixel2d = pixel2d self.width = width self.height = height @@ -62,7 +69,7 @@ def render_year_menu(self) -> None: items=["Year"] + [str(year) for year in range(2018, self.current_year + 1)], item_scale=0.7, initialitem=0, - pos=Point3(0, 0, -self.height / 2) + pos=Point3(0, 0, -self.height / 2), ) self.year_menu["command"] = self.select_year @@ -77,7 +84,7 @@ def select_event(self, event_name: str) -> None: self.data_extractor.event_name = event_name event = self.data_extractor.event - match event['EventFormat']: + match event["EventFormat"]: case "sprint_qualifying": self.session_menu["items"] = ["Session"] + SprintQualifyingSessionIdentifiers.all_values() case "conventional": @@ -96,7 +103,7 @@ def render_events_menu(self) -> None: items=["Event"], item_scale=0.7, initialitem=0, - pos=Point3(76, 0, -self.height / 2) + pos=Point3(76, 0, -self.height / 2), ) def select_session(self, session_id: str) -> None: @@ -121,7 +128,7 @@ def render_session_menu(self) -> None: items=["Session"], item_scale=0.7, initialitem=0, - pos=Point3(606, 0, -self.height / 2) + pos=Point3(606, 0, -self.height / 2), ) def render(self): diff --git a/src/f1p/ui/components/playback.py b/src/f1p/ui/components/playback.py index 17b3b70..a990d0b 100644 --- a/src/f1p/ui/components/playback.py +++ b/src/f1p/ui/components/playback.py @@ -1,4 +1,4 @@ -from math import sin, cos +from math import cos, sin from direct.gui.DirectButton import DirectButton from direct.gui.DirectFrame import DirectFrame @@ -7,13 +7,11 @@ from direct.showbase.DirectObject import DirectObject from direct.showbase.MessengerGlobal import messenger from direct.task.Task import TaskManager -from panda3d.core import Point3, StaticTextFont, Camera, deg2Rad, TextNode +from panda3d.core import Camera, Point3, StaticTextFont, TextNode, deg2Rad from f1p.services.data_extractor import DataExtractorService from f1p.ui.components.gui.button import BlackButton from f1p.ui.components.gui.drop_down import BlackDropDown -from f1p.ui.components.leaderboard import Leaderboard -from f1p.ui.components.map import Map class PlaybackControls(DirectObject): @@ -27,7 +25,7 @@ def __init__( height: int, symbols_font: StaticTextFont, text_font: StaticTextFont, - data_extractor: DataExtractorService + data_extractor: DataExtractorService, ): super().__init__() @@ -60,7 +58,7 @@ def render_frame(self) -> None: parent=self.pixel2d, frameColor=(0.18, 0.18, 0.18, 1), frameSize=(0, self.width, 0, -self.height), - pos=Point3(0, 0, self.height - self.window_height) + pos=Point3(0, 0, self.height - self.window_height), ) def move_timeline(self, task): @@ -94,7 +92,7 @@ def render_play_button(self) -> None: text_scale=self.height - 5, text_align=TextNode.ACenter, text_pos=(-2, (-self.height / 2) + 7), - pos=Point3(17, 0, -self.height / 2) + pos=Point3(17, 0, -self.height / 2), ) def update_components(self) -> None: @@ -120,7 +118,7 @@ def render_timeline(self) -> None: frameColor=record.Color, frameSize=(0, record.Width, 0, -3), pos=Point3(record.PixelStart, 0, 0), - ) + ), ) self.timeline = DirectSlider( @@ -128,7 +126,7 @@ def render_timeline(self) -> None: value=1, range=(1, self.data_extractor.session_ticks), pageSize=1, - frameSize=(0, self.width - 121, -self.height/ 2, self.height/ 2), + frameSize=(0, self.width - 121, -self.height / 2, self.height / 2), frameColor=(0.15, 0.15, 0.15, 1), thumb_frameSize=(0, 5, -self.height / 2, self.height / 2), thumb_frameColor=(0.1, 0.1, 0.1, 1), @@ -137,7 +135,7 @@ def render_timeline(self) -> None: text_scale=self.height, text_fg=(1, 1, 1, 1), text_pos=(-2, (-self.height / 2) + 7), - pos=Point3(34, 0, -self.height / 2) + pos=Point3(34, 0, -self.height / 2), ) def change_playback_speed(self, playback_speed: str) -> None: @@ -165,7 +163,7 @@ def render_playback_speed_button(self) -> None: items=["x3", "x5", "x10"], item_scale=1.0, initialitem=0, - pos=Point3(self.width - 87, 0, -self.height / 2) + pos=Point3(self.width - 87, 0, -self.height / 2), ) def move_camera(self, task): @@ -213,7 +211,7 @@ def render_camera_button(self) -> None: items=["🌎", "🗺"], item_scale=1.0, initialitem=0, - pos=Point3(self.width - 40, 0, -self.height / 2) + pos=Point3(self.width - 40, 0, -self.height / 2), ) def render(self): diff --git a/src/f1p/utils/color.py b/src/f1p/utils/color.py index ebcb965..9651ddb 100644 --- a/src/f1p/utils/color.py +++ b/src/f1p/utils/color.py @@ -3,19 +3,19 @@ def hex_to_rgb_saturation(hex_code): # 1. Convert hex to RGB (0-255 range) - hex_code = hex_code.lstrip('#') + hex_code = hex_code.lstrip("#") lv = len(hex_code) - rgb_255 = tuple(int(hex_code[i:i + lv // 3], 16) for i in range(0, lv, lv // 3)) + rgb_255 = tuple(int(hex_code[i : i + lv // 3], 16) for i in range(0, lv, lv // 3)) # 2. Normalize RGB to the [0.0, 1.0] range r_norm, g_norm, b_norm = [x / 255.0 for x in rgb_255] # 3. Convert normalized RGB to HLS (Hue, Lightness, Saturation) # The result 's' is the saturation, ranging from 0.0 (grayscale) to 1.0 (full color) - h, l, s = colorsys.rgb_to_hls(r_norm, g_norm, b_norm) + _, _, saturation = colorsys.rgb_to_hls(r_norm, g_norm, b_norm) return { "hex": f"#{hex_code}", "rgb": rgb_255, - "saturation_hls": s - } \ No newline at end of file + "saturation_hls": saturation, + } diff --git a/src/f1p/utils/geometry.py b/src/f1p/utils/geometry.py index c7a7af6..ce012ca 100644 --- a/src/f1p/utils/geometry.py +++ b/src/f1p/utils/geometry.py @@ -10,6 +10,7 @@ def scale(df: DataFrame, factor: float) -> DataFrame: return new_df + def shift(df: DataFrame, direction: str, amount: float) -> DataFrame: new_df = df.copy() new_df[direction] = new_df[direction] + amount @@ -18,14 +19,14 @@ def shift(df: DataFrame, direction: str, amount: float) -> DataFrame: def rotate(df: DataFrame, radians: float) -> DataFrame: - coordinates = df.loc[:, ('X', 'Y', 'Z')].to_numpy() + coordinates = df.loc[:, ("X", "Y", "Z")].to_numpy() rot_mat = np.array( [ [np.cos(radians), np.sin(radians), 0], [-np.sin(radians), np.cos(radians), 0], [0, 0, 1], - ] + ], ) rotated_coordinates = np.matmul(coordinates, rot_mat) @@ -34,9 +35,10 @@ def rotate(df: DataFrame, radians: float) -> DataFrame: "X": rotated_coordinates[:, 0], "Y": rotated_coordinates[:, 1], "Z": rotated_coordinates[:, 2], - } + }, ) + def find_center(df: DataFrame) -> tuple[float, float, float]: return ( ((df["X"].max() - df["X"].min()) / 2) + df["X"].min(), @@ -44,10 +46,11 @@ def find_center(df: DataFrame) -> tuple[float, float, float]: ((df["Z"].max() - df["Z"].min()) / 2) + df["Z"].min(), ) + def resize_pos_data(rotation: float, pos_data_df: DataFrame) -> DataFrame: df = pos_data_df.copy() - coordinates_cols_only_df = df[['X', 'Y', 'Z']] + coordinates_cols_only_df = df[["X", "Y", "Z"]] rotated_coordinates_df = rotate(coordinates_cols_only_df, rotation) scaled_coordinates_df = scale(rotated_coordinates_df, 1 / 600) @@ -58,9 +61,10 @@ def resize_pos_data(rotation: float, pos_data_df: DataFrame) -> DataFrame: return df + def center_pos_data(map_center_coordinate: tuple[float, float, float], df: DataFrame) -> DataFrame: combined_pos_data_df = df.copy() - coordinates_cols_only_df = combined_pos_data_df[['X', 'Y', 'Z']] + coordinates_cols_only_df = combined_pos_data_df[["X", "Y", "Z"]] shifted_x_coordinates_df = shift(coordinates_cols_only_df, direction="X", amount=-map_center_coordinate[0]) shifted_y_coordinates_df = shift(coordinates_cols_only_df, direction="Y", amount=-map_center_coordinate[1]) diff --git a/src/f1p/utils/performance.py b/src/f1p/utils/performance.py index afa4a4a..6a11ee8 100644 --- a/src/f1p/utils/performance.py +++ b/src/f1p/utils/performance.py @@ -1,16 +1,19 @@ import time from functools import wraps + def timeit(func): """ Decorator to measure the execution time of a function. """ + @wraps(func) def wrapper(*args, **kwargs): start_time = time.perf_counter() # Record the start time - result = func(*args, **kwargs) # Execute the function - end_time = time.perf_counter() # Record the end time + result = func(*args, **kwargs) # Execute the function + end_time = time.perf_counter() # Record the end time execution_time = end_time - start_time - print(f"'{func.__name__}' took {execution_time:.4f} seconds to complete") + print(f"'{func.__name__}' took {execution_time:.4f} seconds to complete") # noqa: T201 return result - return wrapper \ No newline at end of file + + return wrapper diff --git a/tests/f1p/services/data_extractor/enums/test_conventional_session_identifiers.py b/tests/f1p/services/data_extractor/enums/test_conventional_session_identifiers.py index 79a7a85..34d031f 100644 --- a/tests/f1p/services/data_extractor/enums/test_conventional_session_identifiers.py +++ b/tests/f1p/services/data_extractor/enums/test_conventional_session_identifiers.py @@ -8,12 +8,12 @@ @pytest.mark.parametrize( ("value", "expected"), [ - ["Practice 1", ConventionalSessionIdentifiers.FREE_PRACTICE_1], - ["Practice 2", ConventionalSessionIdentifiers.FREE_PRACTICE_2], - ["Practice 3", ConventionalSessionIdentifiers.FREE_PRACTICE_3], - ["Qualifying", ConventionalSessionIdentifiers.QUALIFYING], - ["Race", ConventionalSessionIdentifiers.RACE], - ] + ("Practice 1", ConventionalSessionIdentifiers.FREE_PRACTICE_1), + ("Practice 2", ConventionalSessionIdentifiers.FREE_PRACTICE_2), + ("Practice 3", ConventionalSessionIdentifiers.FREE_PRACTICE_3), + ("Qualifying", ConventionalSessionIdentifiers.QUALIFYING), + ("Race", ConventionalSessionIdentifiers.RACE), + ], ) def test_initialization(value: str, expected: ConventionalSessionIdentifiers) -> None: enum = ConventionalSessionIdentifiers(value) From ec9b33ade05436e0afe94f322bdcc167678c4145 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Thu, 15 Jan 2026 21:48:12 -0500 Subject: [PATCH 39/45] cleanup test coverage settings --- pyproject.toml | 11 +++++++++-- src/f1p/ui/components/driver.py | 2 +- src/{f1p/services => }/procedural3d/__init__.py | 0 src/{f1p/services => }/procedural3d/base.py | 0 src/{f1p/services => }/procedural3d/box.py | 0 src/{f1p/services => }/procedural3d/cone.py | 0 src/{f1p/services => }/procedural3d/cylinder.py | 0 src/{f1p/services => }/procedural3d/sphere.py | 0 src/{f1p/services => }/procedural3d/torus.py | 0 9 files changed, 10 insertions(+), 3 deletions(-) rename src/{f1p/services => }/procedural3d/__init__.py (100%) rename src/{f1p/services => }/procedural3d/base.py (100%) rename src/{f1p/services => }/procedural3d/box.py (100%) rename src/{f1p/services => }/procedural3d/cone.py (100%) rename src/{f1p/services => }/procedural3d/cylinder.py (100%) rename src/{f1p/services => }/procedural3d/sphere.py (100%) rename src/{f1p/services => }/procedural3d/torus.py (100%) diff --git a/pyproject.toml b/pyproject.toml index 1854002..fa03325 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,6 @@ fastf1 = "^3.7.0" panda3d = "^1.10.15" pandas-stubs = "^2.3.3.251219" - [tool.poetry.group.dev.dependencies] ruff = "^0.12.8" pytest = "8.4.1" @@ -33,13 +32,21 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] f1p = "f1p.main:app" +[tool.coverage.run] +branch = true +relative_files = true +omit = [ + "tests/*", + "src/procedural3d", +] + [tool.ruff] target-version = "py314" line-length = 120 src = ["src", "tests"] preview = true exclude = [ - "src/f1p/services/procedural3d" + "src/procedural3d" ] [tool.ruff.lint] diff --git a/src/f1p/ui/components/driver.py b/src/f1p/ui/components/driver.py index d097cbe..1b5c940 100644 --- a/src/f1p/ui/components/driver.py +++ b/src/f1p/ui/components/driver.py @@ -5,7 +5,7 @@ from panda3d.core import LVecBase4f, NodePath from pandas import Series, Timedelta -from f1p.services.procedural3d import SphereMaker +from procedural3d import SphereMaker from f1p.utils.color import hex_to_rgb_saturation diff --git a/src/f1p/services/procedural3d/__init__.py b/src/procedural3d/__init__.py similarity index 100% rename from src/f1p/services/procedural3d/__init__.py rename to src/procedural3d/__init__.py diff --git a/src/f1p/services/procedural3d/base.py b/src/procedural3d/base.py similarity index 100% rename from src/f1p/services/procedural3d/base.py rename to src/procedural3d/base.py diff --git a/src/f1p/services/procedural3d/box.py b/src/procedural3d/box.py similarity index 100% rename from src/f1p/services/procedural3d/box.py rename to src/procedural3d/box.py diff --git a/src/f1p/services/procedural3d/cone.py b/src/procedural3d/cone.py similarity index 100% rename from src/f1p/services/procedural3d/cone.py rename to src/procedural3d/cone.py diff --git a/src/f1p/services/procedural3d/cylinder.py b/src/procedural3d/cylinder.py similarity index 100% rename from src/f1p/services/procedural3d/cylinder.py rename to src/procedural3d/cylinder.py diff --git a/src/f1p/services/procedural3d/sphere.py b/src/procedural3d/sphere.py similarity index 100% rename from src/f1p/services/procedural3d/sphere.py rename to src/procedural3d/sphere.py diff --git a/src/f1p/services/procedural3d/torus.py b/src/procedural3d/torus.py similarity index 100% rename from src/f1p/services/procedural3d/torus.py rename to src/procedural3d/torus.py From c8bb80a652900eb7d805c5f41b98f41f3afd95c1 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Thu, 15 Jan 2026 21:50:47 -0500 Subject: [PATCH 40/45] moved app --- pyproject.toml | 2 +- src/f1p/__init__.py | 93 +++++++++++++++++++++++++++++++++++++++++++++ src/f1p/main.py | 93 --------------------------------------------- 3 files changed, 94 insertions(+), 94 deletions(-) delete mode 100644 src/f1p/main.py diff --git a/pyproject.toml b/pyproject.toml index fa03325..c81856e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] -f1p = "f1p.main:app" +f1p = "f1p:app" [tool.coverage.run] branch = true diff --git a/src/f1p/__init__.py b/src/f1p/__init__.py index e69de29..9a220f5 100644 --- a/src/f1p/__init__.py +++ b/src/f1p/__init__.py @@ -0,0 +1,93 @@ +from typing import Self + +from direct.showbase.ShowBase import ShowBase +from panda3d.core import WindowProperties + +from f1p.services.data_extractor import DataExtractorService +from f1p.ui.components.leaderboard import Leaderboard +from f1p.ui.components.map import Map +from f1p.ui.components.menu import Menu +from f1p.ui.components.origin import Origin +from f1p.ui.components.playback import PlaybackControls + + +class F1PlayerApp(ShowBase): + def __init__(self, width: int = 800, height: int = 800, draw_origin: bool = False): + super().__init__(self) + + self.symbols_font = self.loader.loadFont("./src/f1p/ui/fonts/NotoSansSymbols2-Regular.ttf") + self.text_font = self.loader.loadFont("./src/f1p/ui/fonts/f1_font.ttf") + + self.width = width + self.height = height + + self._data_extractor: DataExtractorService | None = None + self.cam.setPos(0, -70, 40) + self.cam.lookAt(0, 0, 0) + + self.setBackgroundColor(0.3, 0.3, 0.3, 1) + + # self.setFrameRateMeter(True) + # PStatClient.connect() + + self.ui_components: list = [] + + if draw_origin: + origin = Origin(self.render) + origin.render() + + @property + def data_extractor(self) -> DataExtractorService: + if self._data_extractor is None: + self._data_extractor = DataExtractorService() + + return self._data_extractor + + def configure_window(self) -> Self: + props = WindowProperties() + props.setSize(self.width, self.height) + self.win.requestProperties(props) + + return self + + def draw_menu(self) -> Self: + menu = Menu(self.pixel2d, self.width, 40, self.text_font, self.data_extractor) + menu.render() + + return self + + def register_ui_components(self) -> Self: + playback_controls = PlaybackControls( + self.pixel2d, + self.cam, + self.taskMgr, + self.height, + self.width, + 30, + self.symbols_font, + self.text_font, + self.data_extractor, + ) + + circuit_map = Map(self.render, self.data_extractor) + + leaderboard = Leaderboard( + self.pixel2d, + self.symbols_font, + self.text_font, + circuit_map, + self.data_extractor, + ) + + self.ui_components = [ + playback_controls, + circuit_map, + leaderboard, + ] + + return self + + +app = F1PlayerApp() +app.disableMouse() # disable camera controls +(app.configure_window().draw_menu().register_ui_components().run()) diff --git a/src/f1p/main.py b/src/f1p/main.py deleted file mode 100644 index 9a220f5..0000000 --- a/src/f1p/main.py +++ /dev/null @@ -1,93 +0,0 @@ -from typing import Self - -from direct.showbase.ShowBase import ShowBase -from panda3d.core import WindowProperties - -from f1p.services.data_extractor import DataExtractorService -from f1p.ui.components.leaderboard import Leaderboard -from f1p.ui.components.map import Map -from f1p.ui.components.menu import Menu -from f1p.ui.components.origin import Origin -from f1p.ui.components.playback import PlaybackControls - - -class F1PlayerApp(ShowBase): - def __init__(self, width: int = 800, height: int = 800, draw_origin: bool = False): - super().__init__(self) - - self.symbols_font = self.loader.loadFont("./src/f1p/ui/fonts/NotoSansSymbols2-Regular.ttf") - self.text_font = self.loader.loadFont("./src/f1p/ui/fonts/f1_font.ttf") - - self.width = width - self.height = height - - self._data_extractor: DataExtractorService | None = None - self.cam.setPos(0, -70, 40) - self.cam.lookAt(0, 0, 0) - - self.setBackgroundColor(0.3, 0.3, 0.3, 1) - - # self.setFrameRateMeter(True) - # PStatClient.connect() - - self.ui_components: list = [] - - if draw_origin: - origin = Origin(self.render) - origin.render() - - @property - def data_extractor(self) -> DataExtractorService: - if self._data_extractor is None: - self._data_extractor = DataExtractorService() - - return self._data_extractor - - def configure_window(self) -> Self: - props = WindowProperties() - props.setSize(self.width, self.height) - self.win.requestProperties(props) - - return self - - def draw_menu(self) -> Self: - menu = Menu(self.pixel2d, self.width, 40, self.text_font, self.data_extractor) - menu.render() - - return self - - def register_ui_components(self) -> Self: - playback_controls = PlaybackControls( - self.pixel2d, - self.cam, - self.taskMgr, - self.height, - self.width, - 30, - self.symbols_font, - self.text_font, - self.data_extractor, - ) - - circuit_map = Map(self.render, self.data_extractor) - - leaderboard = Leaderboard( - self.pixel2d, - self.symbols_font, - self.text_font, - circuit_map, - self.data_extractor, - ) - - self.ui_components = [ - playback_controls, - circuit_map, - leaderboard, - ] - - return self - - -app = F1PlayerApp() -app.disableMouse() # disable camera controls -(app.configure_window().draw_menu().register_ui_components().run()) From abcb4d6e2dc0edf7983e68f7fd895652694ee3b3 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Thu, 15 Jan 2026 21:52:20 -0500 Subject: [PATCH 41/45] fix linter --- src/f1p/ui/components/driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/f1p/ui/components/driver.py b/src/f1p/ui/components/driver.py index 1b5c940..623cc33 100644 --- a/src/f1p/ui/components/driver.py +++ b/src/f1p/ui/components/driver.py @@ -5,8 +5,8 @@ from panda3d.core import LVecBase4f, NodePath from pandas import Series, Timedelta -from procedural3d import SphereMaker from f1p.utils.color import hex_to_rgb_saturation +from procedural3d import SphereMaker class Driver(DirectObject): From c186b4f828919d263f875d6786c6f47f1607ebb8 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Thu, 15 Jan 2026 21:54:22 -0500 Subject: [PATCH 42/45] main has to stay --- src/f1p/__init__.py | 93 --------------------------------------------- src/f1p/main.py | 93 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 93 deletions(-) create mode 100644 src/f1p/main.py diff --git a/src/f1p/__init__.py b/src/f1p/__init__.py index 9a220f5..e69de29 100644 --- a/src/f1p/__init__.py +++ b/src/f1p/__init__.py @@ -1,93 +0,0 @@ -from typing import Self - -from direct.showbase.ShowBase import ShowBase -from panda3d.core import WindowProperties - -from f1p.services.data_extractor import DataExtractorService -from f1p.ui.components.leaderboard import Leaderboard -from f1p.ui.components.map import Map -from f1p.ui.components.menu import Menu -from f1p.ui.components.origin import Origin -from f1p.ui.components.playback import PlaybackControls - - -class F1PlayerApp(ShowBase): - def __init__(self, width: int = 800, height: int = 800, draw_origin: bool = False): - super().__init__(self) - - self.symbols_font = self.loader.loadFont("./src/f1p/ui/fonts/NotoSansSymbols2-Regular.ttf") - self.text_font = self.loader.loadFont("./src/f1p/ui/fonts/f1_font.ttf") - - self.width = width - self.height = height - - self._data_extractor: DataExtractorService | None = None - self.cam.setPos(0, -70, 40) - self.cam.lookAt(0, 0, 0) - - self.setBackgroundColor(0.3, 0.3, 0.3, 1) - - # self.setFrameRateMeter(True) - # PStatClient.connect() - - self.ui_components: list = [] - - if draw_origin: - origin = Origin(self.render) - origin.render() - - @property - def data_extractor(self) -> DataExtractorService: - if self._data_extractor is None: - self._data_extractor = DataExtractorService() - - return self._data_extractor - - def configure_window(self) -> Self: - props = WindowProperties() - props.setSize(self.width, self.height) - self.win.requestProperties(props) - - return self - - def draw_menu(self) -> Self: - menu = Menu(self.pixel2d, self.width, 40, self.text_font, self.data_extractor) - menu.render() - - return self - - def register_ui_components(self) -> Self: - playback_controls = PlaybackControls( - self.pixel2d, - self.cam, - self.taskMgr, - self.height, - self.width, - 30, - self.symbols_font, - self.text_font, - self.data_extractor, - ) - - circuit_map = Map(self.render, self.data_extractor) - - leaderboard = Leaderboard( - self.pixel2d, - self.symbols_font, - self.text_font, - circuit_map, - self.data_extractor, - ) - - self.ui_components = [ - playback_controls, - circuit_map, - leaderboard, - ] - - return self - - -app = F1PlayerApp() -app.disableMouse() # disable camera controls -(app.configure_window().draw_menu().register_ui_components().run()) diff --git a/src/f1p/main.py b/src/f1p/main.py new file mode 100644 index 0000000..9a220f5 --- /dev/null +++ b/src/f1p/main.py @@ -0,0 +1,93 @@ +from typing import Self + +from direct.showbase.ShowBase import ShowBase +from panda3d.core import WindowProperties + +from f1p.services.data_extractor import DataExtractorService +from f1p.ui.components.leaderboard import Leaderboard +from f1p.ui.components.map import Map +from f1p.ui.components.menu import Menu +from f1p.ui.components.origin import Origin +from f1p.ui.components.playback import PlaybackControls + + +class F1PlayerApp(ShowBase): + def __init__(self, width: int = 800, height: int = 800, draw_origin: bool = False): + super().__init__(self) + + self.symbols_font = self.loader.loadFont("./src/f1p/ui/fonts/NotoSansSymbols2-Regular.ttf") + self.text_font = self.loader.loadFont("./src/f1p/ui/fonts/f1_font.ttf") + + self.width = width + self.height = height + + self._data_extractor: DataExtractorService | None = None + self.cam.setPos(0, -70, 40) + self.cam.lookAt(0, 0, 0) + + self.setBackgroundColor(0.3, 0.3, 0.3, 1) + + # self.setFrameRateMeter(True) + # PStatClient.connect() + + self.ui_components: list = [] + + if draw_origin: + origin = Origin(self.render) + origin.render() + + @property + def data_extractor(self) -> DataExtractorService: + if self._data_extractor is None: + self._data_extractor = DataExtractorService() + + return self._data_extractor + + def configure_window(self) -> Self: + props = WindowProperties() + props.setSize(self.width, self.height) + self.win.requestProperties(props) + + return self + + def draw_menu(self) -> Self: + menu = Menu(self.pixel2d, self.width, 40, self.text_font, self.data_extractor) + menu.render() + + return self + + def register_ui_components(self) -> Self: + playback_controls = PlaybackControls( + self.pixel2d, + self.cam, + self.taskMgr, + self.height, + self.width, + 30, + self.symbols_font, + self.text_font, + self.data_extractor, + ) + + circuit_map = Map(self.render, self.data_extractor) + + leaderboard = Leaderboard( + self.pixel2d, + self.symbols_font, + self.text_font, + circuit_map, + self.data_extractor, + ) + + self.ui_components = [ + playback_controls, + circuit_map, + leaderboard, + ] + + return self + + +app = F1PlayerApp() +app.disableMouse() # disable camera controls +(app.configure_window().draw_menu().register_ui_components().run()) From 109580df484bd985c8659f854894d3a256674f6a Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Thu, 15 Jan 2026 21:55:42 -0500 Subject: [PATCH 43/45] add pipefail --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a5b8c2..00b0c02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -141,6 +141,7 @@ jobs: - name: Run tests with coverage run: | source ./.venv/bin/activate + set -o pipefail poetry run pytest ./tests --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=./src | tee pytest-coverage.txt - name: Pytest Coverage Comment From 1d607cf4512602012654cc855d1332b152b515ea Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Thu, 15 Jan 2026 21:56:38 -0500 Subject: [PATCH 44/45] change cov param --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00b0c02..e206355 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -142,7 +142,7 @@ jobs: run: | source ./.venv/bin/activate set -o pipefail - poetry run pytest ./tests --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=./src | tee pytest-coverage.txt + poetry run pytest ./tests --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=f1p | tee pytest-coverage.txt - name: Pytest Coverage Comment uses: MishaKav/pytest-coverage-comment@v1.2.0 From bbec282100bd948210a81f06dff7ed428d97b9d1 Mon Sep 17 00:00:00 2001 From: Mitko Tochev Date: Thu, 15 Jan 2026 22:01:01 -0500 Subject: [PATCH 45/45] always add new comment --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e206355..9c48df5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -150,3 +150,4 @@ jobs: pytest-coverage-path: ./pytest-coverage.txt junitxml-path: ./pytest.xml github-token: ${{ secrets.GITHUB_TOKEN }} + create-new-comment: true