From d6453ccd4e7a6cbe3767d0f7d0c7094d2045c474 Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Mon, 9 Mar 2026 10:35:24 +0100 Subject: [PATCH 01/21] alpha release --- .github/workflows/CI.yml | 181 + .gitignore | 72 + .pre-commit-config.yaml | 26 + .rust-analyzer.toml | 2 + Cargo.lock | 3419 ++++++++++++ Cargo.toml | 20 + LICENSE | 0 Makefile | 118 + README.md | 154 + crates/tdc-core/Cargo.toml | 40 + crates/tdc-core/benches/ops.rs | 151 + crates/tdc-core/src/config.rs | 27 + crates/tdc-core/src/datacontainer/builder.rs | 1382 +++++ crates/tdc-core/src/datacontainer/core.rs | 404 ++ crates/tdc-core/src/datacontainer/lazy.rs | 341 ++ crates/tdc-core/src/datacontainer/mod.rs | 7 + crates/tdc-core/src/error.rs | 147 + crates/tdc-core/src/io.rs | 91 + crates/tdc-core/src/lib.rs | 13 + crates/tdc-core/src/schema/attribute.rs | 156 + .../tdc-core/src/schema/coordinate_system.rs | 216 + crates/tdc-core/src/schema/entity.rs | 28 + crates/tdc-core/src/schema/field.rs | 150 + crates/tdc-core/src/schema/fields.rs | 355 ++ crates/tdc-core/src/schema/mod.rs | 13 + crates/tdc-core/src/schema/period.rs | 20 + crates/tdc-core/src/selector/expr.rs | 128 + crates/tdc-core/src/selector/mod.rs | 19 + crates/tdc-core/tests/test_builder.rs | 84 + crates/tdc-core/tests/test_datacontainer.rs | 308 ++ crates/tdc-core/tests/test_fields.rs | 54 + crates/tdc-core/tests/test_schema.rs | 142 + crates/tdc-ops/Cargo.toml | 20 + crates/tdc-ops/src/lib.rs | 277 + crates/tdc-parsers/Cargo.toml | 38 + crates/tdc-parsers/benches/memory.rs | 40 + crates/tdc-parsers/benches/parsers.rs | 20 + crates/tdc-parsers/src/lib.rs | 42 + crates/tdc-parsers/src/sportec.rs | 474 ++ crates/tdc-parsers/src/statsperform.rs | 162 + crates/tdc-parsers/tests/test_sportec.rs | 202 + crates/tdc-parsers/tests/test_statsperform.rs | 140 + crates/tdc-python/Cargo.toml | 32 + crates/tdc-python/src/builder.rs | 84 + crates/tdc-python/src/container.rs | 752 +++ crates/tdc-python/src/coordinate_system.rs | 358 ++ crates/tdc-python/src/error.rs | 92 + crates/tdc-python/src/io.rs | 7 + crates/tdc-python/src/lib.rs | 31 + crates/tdc-python/src/mod.rs | 20 + crates/tdc-python/src/ops.rs | 19 + crates/tdc-python/src/schema.rs | 208 + crates/tdc-python/src/selector.rs | 330 ++ crates/tdc-python/src/udf.rs | 169 + examples/01-basics.ipynb | 359 ++ examples/02-parsing.ipynb | 267 + examples/03-interop.ipynb | 228 + examples/04-udfs.ipynb | 315 ++ examples/05-lazy.ipynb | 183 + pyproject.toml | 120 + python/tests/benchmarks/conftest.py | 12 + .../benchmarks/test_add_column_benchmark.py | 156 + .../tests/benchmarks/test_lazy_filtering.py | 85 + python/tests/benchmarks/test_sportec.py | 123 + python/tests/benchmarks/test_statsperform.py | 153 + python/tests/conftest.py | 38 + python/tests/test_add_column.py | 69 + python/tests/test_build.py | 93 + python/tests/test_container.py | 159 + python/tests/test_errors.py | 15 + python/tests/test_interop.py | 61 + python/tests/test_ops.py | 73 + python/tests/test_schema.py | 303 + python/tests/test_select.py | 78 + python/tests/test_transform.py | 179 + python/tracking_data_container/__init__.py | 65 + .../_tracking_data_container.pyi | 286 + python/tracking_data_container/builder.py | 90 + python/tracking_data_container/container.py | 1079 ++++ .../coordinate_system.py | 233 + python/tracking_data_container/exceptions.py | 13 + python/tracking_data_container/io.py | 77 + python/tracking_data_container/py.typed | 0 python/tracking_data_container/schema.py | 328 ++ python/tracking_data_container/selector.py | 62 + tests/test_builder.rs | 118 + uv.lock | 4916 +++++++++++++++++ 87 files changed, 22121 insertions(+) create mode 100644 .github/workflows/CI.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .rust-analyzer.toml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 crates/tdc-core/Cargo.toml create mode 100644 crates/tdc-core/benches/ops.rs create mode 100644 crates/tdc-core/src/config.rs create mode 100644 crates/tdc-core/src/datacontainer/builder.rs create mode 100644 crates/tdc-core/src/datacontainer/core.rs create mode 100644 crates/tdc-core/src/datacontainer/lazy.rs create mode 100644 crates/tdc-core/src/datacontainer/mod.rs create mode 100644 crates/tdc-core/src/error.rs create mode 100644 crates/tdc-core/src/io.rs create mode 100644 crates/tdc-core/src/lib.rs create mode 100644 crates/tdc-core/src/schema/attribute.rs create mode 100644 crates/tdc-core/src/schema/coordinate_system.rs create mode 100644 crates/tdc-core/src/schema/entity.rs create mode 100644 crates/tdc-core/src/schema/field.rs create mode 100644 crates/tdc-core/src/schema/fields.rs create mode 100644 crates/tdc-core/src/schema/mod.rs create mode 100644 crates/tdc-core/src/schema/period.rs create mode 100644 crates/tdc-core/src/selector/expr.rs create mode 100644 crates/tdc-core/src/selector/mod.rs create mode 100644 crates/tdc-core/tests/test_builder.rs create mode 100644 crates/tdc-core/tests/test_datacontainer.rs create mode 100644 crates/tdc-core/tests/test_fields.rs create mode 100644 crates/tdc-core/tests/test_schema.rs create mode 100644 crates/tdc-ops/Cargo.toml create mode 100644 crates/tdc-ops/src/lib.rs create mode 100644 crates/tdc-parsers/Cargo.toml create mode 100644 crates/tdc-parsers/benches/memory.rs create mode 100644 crates/tdc-parsers/benches/parsers.rs create mode 100644 crates/tdc-parsers/src/lib.rs create mode 100644 crates/tdc-parsers/src/sportec.rs create mode 100644 crates/tdc-parsers/src/statsperform.rs create mode 100644 crates/tdc-parsers/tests/test_sportec.rs create mode 100644 crates/tdc-parsers/tests/test_statsperform.rs create mode 100644 crates/tdc-python/Cargo.toml create mode 100644 crates/tdc-python/src/builder.rs create mode 100644 crates/tdc-python/src/container.rs create mode 100644 crates/tdc-python/src/coordinate_system.rs create mode 100644 crates/tdc-python/src/error.rs create mode 100644 crates/tdc-python/src/io.rs create mode 100644 crates/tdc-python/src/lib.rs create mode 100644 crates/tdc-python/src/mod.rs create mode 100644 crates/tdc-python/src/ops.rs create mode 100644 crates/tdc-python/src/schema.rs create mode 100644 crates/tdc-python/src/selector.rs create mode 100644 crates/tdc-python/src/udf.rs create mode 100644 examples/01-basics.ipynb create mode 100644 examples/02-parsing.ipynb create mode 100644 examples/03-interop.ipynb create mode 100644 examples/04-udfs.ipynb create mode 100644 examples/05-lazy.ipynb create mode 100644 pyproject.toml create mode 100644 python/tests/benchmarks/conftest.py create mode 100644 python/tests/benchmarks/test_add_column_benchmark.py create mode 100644 python/tests/benchmarks/test_lazy_filtering.py create mode 100644 python/tests/benchmarks/test_sportec.py create mode 100644 python/tests/benchmarks/test_statsperform.py create mode 100644 python/tests/conftest.py create mode 100644 python/tests/test_add_column.py create mode 100644 python/tests/test_build.py create mode 100644 python/tests/test_container.py create mode 100644 python/tests/test_errors.py create mode 100644 python/tests/test_interop.py create mode 100644 python/tests/test_ops.py create mode 100644 python/tests/test_schema.py create mode 100644 python/tests/test_select.py create mode 100644 python/tests/test_transform.py create mode 100644 python/tracking_data_container/__init__.py create mode 100644 python/tracking_data_container/_tracking_data_container.pyi create mode 100644 python/tracking_data_container/builder.py create mode 100644 python/tracking_data_container/container.py create mode 100644 python/tracking_data_container/coordinate_system.py create mode 100644 python/tracking_data_container/exceptions.py create mode 100644 python/tracking_data_container/io.py create mode 100644 python/tracking_data_container/py.typed create mode 100644 python/tracking_data_container/schema.py create mode 100644 python/tracking_data_container/selector.py create mode 100644 tests/test_builder.rs create mode 100644 uv.lock diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..a63ff59 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,181 @@ +# This file is autogenerated by maturin v1.10.2 +# To update, run +# +# maturin generate-ci github +# +name: CI + +on: + push: + branches: + - main + - master + tags: + - '*' + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + linux: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: ubuntu-22.04 + target: x86_64 + - runner: ubuntu-22.04 + target: x86 + - runner: ubuntu-22.04 + target: aarch64 + - runner: ubuntu-22.04 + target: armv7 + - runner: ubuntu-22.04 + target: s390x + - runner: ubuntu-22.04 + target: ppc64le + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + manylinux: auto + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-linux-${{ matrix.platform.target }} + path: dist + + musllinux: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: ubuntu-22.04 + target: x86_64 + - runner: ubuntu-22.04 + target: x86 + - runner: ubuntu-22.04 + target: aarch64 + - runner: ubuntu-22.04 + target: armv7 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + manylinux: musllinux_1_2 + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-musllinux-${{ matrix.platform.target }} + path: dist + + windows: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: windows-latest + target: x64 + - runner: windows-latest + target: x86 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + architecture: ${{ matrix.platform.target }} + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-windows-${{ matrix.platform.target }} + path: dist + + macos: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: macos-13 + target: x86_64 + - runner: macos-14 + target: aarch64 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-macos-${{ matrix.platform.target }} + path: dist + + sdist: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out dist + - name: Upload sdist + uses: actions/upload-artifact@v4 + with: + name: wheels-sdist + path: dist + + release: + name: Release + runs-on: ubuntu-latest + if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }} + needs: [linux, musllinux, windows, macos, sdist] + permissions: + # Use to sign the release artifacts + id-token: write + # Used to upload release artifacts + contents: write + # Used to generate artifact attestation + attestations: write + steps: + - uses: actions/download-artifact@v4 + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v2 + with: + subject-path: 'wheels-*/*' + - name: Publish to PyPI + if: ${{ startsWith(github.ref, 'refs/tags/') }} + uses: PyO3/maturin-action@v1 + env: + MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} + with: + command: upload + args: --non-interactive --skip-existing wheels-*/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b8a3c9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,72 @@ +# Rust +/target +**/*.rs.bk +Cargo.lock.lock + +# Python +__pycache__/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.benchmarks/ +*.py[cod] +*$py.class +*.so +.Python +.venv/ +env/ +venv/ +ENV/ +dist/ +build/ +*.egg-info/ +*.egg +.installed.cfg +pip-log.txt +pip-delete-this-directory.txt +pip-selfcheck.json +.python-version + +# Jupyter Notebook +.ipynb_checkpoints +*/.ipynb_checkpoints/* + +# Coverage / CI +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Documentation +docs/_build/ +site/ + +# IDEs +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +*.log + +# Project Specific +data/ +examples/data/ +python/tracking_data_container/lib_tracking_data_container.dylib.* +python/tracking_data_container/libtracking_data_container.dylib.* +python/tracking_data_container/*.dylib +python/tracking_data_container/*.so diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b07ebd1 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,26 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer +- repo: http://github.com/doublify/pre-commit-rust + rev: v1.0 + hooks: + - id: cargo-check +- repo: local + hooks: + - id: lint + name: Lint + entry: make lint + types_or: [python, rust] + language: system + pass_filenames: false + - id: format + name: Format + entry: make format + types_or: [python, rust] + language: system + pass_filenames: false diff --git a/.rust-analyzer.toml b/.rust-analyzer.toml new file mode 100644 index 0000000..8a79197 --- /dev/null +++ b/.rust-analyzer.toml @@ -0,0 +1,2 @@ +[cargo] +features = ["python"] diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..545fe1d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3419 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "ar_archive_writer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" +dependencies = [ + "object", +] + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "arrow" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5ec52ba94edeed950e4a41f75d35376df196e8cb04437f7280a5aa49f20f796" +dependencies = [ + "arrow-arith", + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-csv", + "arrow-data", + "arrow-ipc", + "arrow-json", + "arrow-ord", + "arrow-row", + "arrow-schema", + "arrow-select", + "arrow-string", +] + +[[package]] +name = "arrow-arith" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc766fdacaf804cb10c7c70580254fcdb5d55cdfda2bc57b02baf5223a3af9e" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "num", +] + +[[package]] +name = "arrow-array" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12fcdb3f1d03f69d3ec26ac67645a8fe3f878d77b5ebb0b15d64a116c212985" +dependencies = [ + "ahash", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "chrono-tz", + "half", + "hashbrown 0.15.5", + "num", +] + +[[package]] +name = "arrow-buffer" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "263f4801ff1839ef53ebd06f99a56cecd1dbaf314ec893d93168e2e860e0291c" +dependencies = [ + "bytes", + "half", + "num", +] + +[[package]] +name = "arrow-cast" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede6175fbc039dfc946a61c1b6d42fd682fcecf5ab5d148fbe7667705798cac9" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "atoi", + "base64", + "chrono", + "comfy-table", + "half", + "lexical-core", + "num", + "ryu", +] + +[[package]] +name = "arrow-csv" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1644877d8bc9a0ef022d9153dc29375c2bda244c39aec05a91d0e87ccf77995f" +dependencies = [ + "arrow-array", + "arrow-cast", + "arrow-schema", + "chrono", + "csv", + "csv-core", + "lazy_static", + "regex", +] + +[[package]] +name = "arrow-data" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61cfdd7d99b4ff618f167e548b2411e5dd2c98c0ddebedd7df433d34c20a4429" +dependencies = [ + "arrow-buffer", + "arrow-schema", + "half", + "num", +] + +[[package]] +name = "arrow-ipc" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ff528658b521e33905334723b795ee56b393dbe9cf76c8b1f64b648c65a60c" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "flatbuffers", + "lz4_flex", +] + +[[package]] +name = "arrow-json" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee5b4ca98a7fb2efb9ab3309a5d1c88b5116997ff93f3147efdc1062a6158e9" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-schema", + "chrono", + "half", + "indexmap", + "lexical-core", + "memchr", + "num", + "serde", + "serde_json", + "simdutf8", +] + +[[package]] +name = "arrow-ord" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0a3334a743bd2a1479dbc635540617a3923b4b2f6870f37357339e6b5363c21" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", +] + +[[package]] +name = "arrow-row" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d1d7a7291d2c5107e92140f75257a99343956871f3d3ab33a7b41532f79cb68" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "half", +] + +[[package]] +name = "arrow-schema" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cfaf5e440be44db5413b75b72c2a87c1f8f0627117d110264048f2969b99e9" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "arrow-select" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69efcd706420e52cd44f5c4358d279801993846d1c2a8e52111853d61d55a619" +dependencies = [ + "ahash", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "num", +] + +[[package]] +name = "arrow-string" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21546b337ab304a32cfc0770f671db7411787586b45b78b4593ae78e64e2b03" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "memchr", + "num", + "regex", + "regex-syntax", +] + +[[package]] +name = "async-compression" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06575e6a9673580f52661c92107baabffbf41e2141373441cbcdc47cb733003c" +dependencies = [ + "bzip2", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "xz2", + "zstd", + "zstd-safe", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bigdecimal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "blake3" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "chrono-tz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" +dependencies = [ + "chrono", + "phf", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "comfy-table" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03b7db8e0b4b2fdad6c551e634134e99ec000e5c8c3b6856c65e8bbaded7a3b" +dependencies = [ + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "datafusion" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae420e7a5b0b7f1c39364cc76cbcd0f5fdc416b2514ae3847c2676bbd60702a" +dependencies = [ + "arrow", + "arrow-array", + "arrow-ipc", + "arrow-schema", + "async-compression", + "async-trait", + "bytes", + "bzip2", + "chrono", + "datafusion-catalog", + "datafusion-common", + "datafusion-common-runtime", + "datafusion-execution", + "datafusion-expr", + "datafusion-functions", + "datafusion-functions-aggregate", + "datafusion-functions-nested", + "datafusion-functions-table", + "datafusion-functions-window", + "datafusion-optimizer", + "datafusion-physical-expr", + "datafusion-physical-expr-common", + "datafusion-physical-optimizer", + "datafusion-physical-plan", + "datafusion-sql", + "flate2", + "futures", + "glob", + "itertools 0.14.0", + "log", + "object_store", + "parking_lot", + "parquet", + "rand", + "regex", + "sqlparser", + "tempfile", + "tokio", + "tokio-util", + "url", + "uuid", + "xz2", + "zstd", +] + +[[package]] +name = "datafusion-catalog" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f27987bc22b810939e8dfecc55571e9d50355d6ea8ec1c47af8383a76a6d0e1" +dependencies = [ + "arrow", + "async-trait", + "dashmap", + "datafusion-common", + "datafusion-execution", + "datafusion-expr", + "datafusion-physical-plan", + "datafusion-sql", + "futures", + "itertools 0.14.0", + "log", + "parking_lot", + "sqlparser", +] + +[[package]] +name = "datafusion-common" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3f6d5b8c9408cc692f7c194b8aa0c0f9b253e065a8d960ad9cdc2a13e697602" +dependencies = [ + "ahash", + "arrow", + "arrow-array", + "arrow-buffer", + "arrow-ipc", + "arrow-schema", + "base64", + "half", + "hashbrown 0.14.5", + "indexmap", + "libc", + "log", + "object_store", + "parquet", + "paste", + "recursive", + "sqlparser", + "tokio", + "web-time", +] + +[[package]] +name = "datafusion-common-runtime" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4603c8e8a4baf77660ab7074cc66fc15cc8a18f2ce9dfadb755fc6ee294e48" +dependencies = [ + "log", + "tokio", +] + +[[package]] +name = "datafusion-doc" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bf4bc68623a5cf231eed601ed6eb41f46a37c4d15d11a0bff24cbc8396cd66" + +[[package]] +name = "datafusion-execution" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b491c012cdf8e051053426013429a76f74ee3c2db68496c79c323ca1084d27" +dependencies = [ + "arrow", + "dashmap", + "datafusion-common", + "datafusion-expr", + "futures", + "log", + "object_store", + "parking_lot", + "rand", + "tempfile", + "url", +] + +[[package]] +name = "datafusion-expr" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a181408d4fc5dc22f9252781a8f39f2d0e5d1b33ec9bde242844980a2689c1" +dependencies = [ + "arrow", + "chrono", + "datafusion-common", + "datafusion-doc", + "datafusion-expr-common", + "datafusion-functions-aggregate-common", + "datafusion-functions-window-common", + "datafusion-physical-expr-common", + "indexmap", + "paste", + "recursive", + "serde_json", + "sqlparser", +] + +[[package]] +name = "datafusion-expr-common" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1129b48e8534d8c03c6543bcdccef0b55c8ac0c1272a15a56c67068b6eb1885" +dependencies = [ + "arrow", + "datafusion-common", + "itertools 0.14.0", + "paste", +] + +[[package]] +name = "datafusion-functions" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6125874e4856dfb09b59886784fcb74cde5cfc5930b3a80a1a728ef7a010df6b" +dependencies = [ + "arrow", + "arrow-buffer", + "base64", + "blake2", + "blake3", + "chrono", + "datafusion-common", + "datafusion-doc", + "datafusion-execution", + "datafusion-expr", + "datafusion-expr-common", + "datafusion-macros", + "hashbrown 0.14.5", + "hex", + "itertools 0.14.0", + "log", + "md-5", + "rand", + "regex", + "sha2", + "unicode-segmentation", + "uuid", +] + +[[package]] +name = "datafusion-functions-aggregate" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3add7b1d3888e05e7c95f2b281af900ca69ebdcb21069ba679b33bde8b3b9d6" +dependencies = [ + "ahash", + "arrow", + "arrow-buffer", + "arrow-schema", + "datafusion-common", + "datafusion-doc", + "datafusion-execution", + "datafusion-expr", + "datafusion-functions-aggregate-common", + "datafusion-macros", + "datafusion-physical-expr", + "datafusion-physical-expr-common", + "half", + "log", + "paste", +] + +[[package]] +name = "datafusion-functions-aggregate-common" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e18baa4cfc3d2f144f74148ed68a1f92337f5072b6dde204a0dbbdf3324989c" +dependencies = [ + "ahash", + "arrow", + "datafusion-common", + "datafusion-expr-common", + "datafusion-physical-expr-common", +] + +[[package]] +name = "datafusion-functions-nested" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ec5ee8cecb0dc370291279673097ddabec03a011f73f30d7f1096457127e03e" +dependencies = [ + "arrow", + "arrow-array", + "arrow-buffer", + "arrow-ord", + "arrow-schema", + "datafusion-common", + "datafusion-doc", + "datafusion-execution", + "datafusion-expr", + "datafusion-functions", + "datafusion-functions-aggregate", + "datafusion-macros", + "datafusion-physical-expr-common", + "itertools 0.14.0", + "log", + "paste", +] + +[[package]] +name = "datafusion-functions-table" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c403ddd473bbb0952ba880008428b3c7febf0ed3ce1eec35a205db20efb2a36" +dependencies = [ + "arrow", + "async-trait", + "datafusion-catalog", + "datafusion-common", + "datafusion-expr", + "datafusion-physical-plan", + "parking_lot", + "paste", +] + +[[package]] +name = "datafusion-functions-window" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab18c2fb835614d06a75f24a9e09136d3a8c12a92d97c95a6af316a1787a9c5" +dependencies = [ + "datafusion-common", + "datafusion-doc", + "datafusion-expr", + "datafusion-functions-window-common", + "datafusion-macros", + "datafusion-physical-expr", + "datafusion-physical-expr-common", + "log", + "paste", +] + +[[package]] +name = "datafusion-functions-window-common" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77b73bc15e7d1967121fdc7a55d819bfb9d6c03766a6c322247dce9094a53a4" +dependencies = [ + "datafusion-common", + "datafusion-physical-expr-common", +] + +[[package]] +name = "datafusion-macros" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09369b8d962291e808977cf94d495fd8b5b38647232d7ef562c27ac0f495b0af" +dependencies = [ + "datafusion-expr", + "quote", + "syn", +] + +[[package]] +name = "datafusion-optimizer" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2403a7e4a84637f3de7d8d4d7a9ccc0cc4be92d89b0161ba3ee5be82f0531c54" +dependencies = [ + "arrow", + "chrono", + "datafusion-common", + "datafusion-expr", + "datafusion-physical-expr", + "indexmap", + "itertools 0.14.0", + "log", + "recursive", + "regex", + "regex-syntax", +] + +[[package]] +name = "datafusion-physical-expr" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ff72ac702b62dbf2650c4e1d715ebd3e4aab14e3885e72e8549e250307347c" +dependencies = [ + "ahash", + "arrow", + "arrow-array", + "arrow-buffer", + "arrow-schema", + "datafusion-common", + "datafusion-expr", + "datafusion-expr-common", + "datafusion-functions-aggregate-common", + "datafusion-physical-expr-common", + "half", + "hashbrown 0.14.5", + "indexmap", + "itertools 0.14.0", + "log", + "paste", + "petgraph", +] + +[[package]] +name = "datafusion-physical-expr-common" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60982b7d684e25579ee29754b4333057ed62e2cc925383c5f0bd8cab7962f435" +dependencies = [ + "ahash", + "arrow", + "arrow-buffer", + "datafusion-common", + "datafusion-expr-common", + "hashbrown 0.14.5", + "itertools 0.14.0", +] + +[[package]] +name = "datafusion-physical-optimizer" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac5e85c189d5238a5cf181a624e450c4cd4c66ac77ca551d6f3ff9080bac90bb" +dependencies = [ + "arrow", + "arrow-schema", + "datafusion-common", + "datafusion-execution", + "datafusion-expr", + "datafusion-expr-common", + "datafusion-physical-expr", + "datafusion-physical-expr-common", + "datafusion-physical-plan", + "futures", + "itertools 0.14.0", + "log", + "recursive", + "url", +] + +[[package]] +name = "datafusion-physical-plan" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c36bf163956d7e2542657c78b3383fdc78f791317ef358a359feffcdb968106f" +dependencies = [ + "ahash", + "arrow", + "arrow-array", + "arrow-buffer", + "arrow-ord", + "arrow-schema", + "async-trait", + "chrono", + "datafusion-common", + "datafusion-common-runtime", + "datafusion-execution", + "datafusion-expr", + "datafusion-functions-window-common", + "datafusion-physical-expr", + "datafusion-physical-expr-common", + "futures", + "half", + "hashbrown 0.14.5", + "indexmap", + "itertools 0.14.0", + "log", + "parking_lot", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "datafusion-sql" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13caa4daede211ecec53c78b13c503b592794d125f9a3cc3afe992edf9e7f43" +dependencies = [ + "arrow", + "arrow-array", + "arrow-schema", + "bigdecimal", + "datafusion-common", + "datafusion-expr", + "indexmap", + "log", + "recursive", + "regex", + "sqlparser", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fast-float" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95765f67b4b18863968b4a1bd5bb576f732b29a4a28c7cd84c09fa3e2875f33c" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flatbuffers" +version = "24.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f1baf0dbf96932ec9a3038d57900329c015b0bfb7b63d904f3bc27e2b02a096" +dependencies = [ + "bitflags 1.3.2", + "rustc_version", +] + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "integer-encoding" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lexical-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56" +dependencies = [ + "lexical-parse-integer", + "lexical-util", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "lexical-util" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17" + +[[package]] +name = "lexical-write-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c438c87c013188d415fbabbb1dceb44249ab81664efbd31b14ae55dabb6361" +dependencies = [ + "lexical-util", + "lexical-write-integer", +] + +[[package]] +name = "lexical-write-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "409851a618475d2d5796377cad353802345cba92c867d9fbcde9cf4eac4e14df" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lz4_flex" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" +dependencies = [ + "twox-hash 2.1.2", +] + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "ndarray" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "numpy" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94caae805f998a07d33af06e6a3891e38556051b8045c615470a71590e13e78" +dependencies = [ + "half", + "libc", + "ndarray", + "num-complex", + "num-integer", + "num-traits", + "pyo3", + "rustc-hash", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "object_store" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cfccb68961a56facde1163f9319e0d15743352344e7808a11795fb99698dcaf" +dependencies = [ + "async-trait", + "bytes", + "chrono", + "futures", + "humantime", + "itertools 0.13.0", + "parking_lot", + "percent-encoding", + "snafu", + "tokio", + "tracing", + "url", + "walkdir", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "parquet" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb15796ac6f56b429fd99e33ba133783ad75b27c36b4b5ce06f1f82cc97754e" +dependencies = [ + "ahash", + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-ipc", + "arrow-schema", + "arrow-select", + "base64", + "brotli", + "bytes", + "chrono", + "flate2", + "futures", + "half", + "hashbrown 0.15.5", + "lz4_flex", + "num", + "num-bigint", + "object_store", + "paste", + "seq-macro", + "simdutf8", + "snap", + "thrift", + "tokio", + "twox-hash 1.6.3", + "zstd", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "pyo3" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7778bffd85cf38175ac1f545509665d0b9b92a198ca7941f131f85f7a4f9a872" +dependencies = [ + "cfg-if", + "chrono", + "chrono-tz", + "indexmap", + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-arrow" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e51f9bb00155f44cd5b1b87199d364613b0cbdf082982f5f0d754287d9b4686" +dependencies = [ + "arrow", + "arrow-array", + "arrow-buffer", + "arrow-schema", + "chrono", + "half", + "indexmap", + "numpy", + "pyo3", + "thiserror", +] + +[[package]] +name = "pyo3-build-config" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f6cbe86ef3bf18998d9df6e0f3fc1050a8c5efa409bf712e661a4366e010fb" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f1b4c431c0bb1c8fb0a338709859eed0d030ff6daa34368d3b152a63dfdd8d" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-log" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45192e5e4a4d2505587e27806c7b710c231c40c56f3bfc19535d0bb25df52264" +dependencies = [ + "arc-swap", + "log", + "pyo3", +] + +[[package]] +name = "pyo3-macros" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc2201328f63c4710f68abdf653c89d8dbc2858b88c5d88b0ff38a75288a9da" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca6726ad0f3da9c9de093d6f116a93c1a38e417ed73bf138472cf4064f72028" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "recursive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0786a43debb760f491b1bc0269fe5e84155353c67482b9e60d0cfb596054b43e" +dependencies = [ + "recursive-proc-macro-impl", + "stacker", +] + +[[package]] +name = "recursive-proc-macro-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76009fbe0614077fc1a2ce255e3a1881a2e3a3527097d5dc6d8212c585e7e38b" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "seq-macro" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "snafu" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "snap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" + +[[package]] +name = "sqlparser" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05a528114c392209b3264855ad491fcce534b94a38771b0a0b97a79379275ce8" +dependencies = [ + "log", + "sqlparser_derive", +] + +[[package]] +name = "sqlparser_derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da5fc6819faabb412da764b99d3b713bb55083c11e7e0c00144d386cd6a1939c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stacker" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tdc-core" +version = "0.1.0-alpha.1" +dependencies = [ + "ahash", + "arrow", + "arrow-arith", + "arrow-array", + "arrow-cast", + "arrow-ord", + "arrow-schema", + "arrow-select", + "chrono", + "criterion", + "datafusion", + "log", + "num_cpus", + "once_cell", + "parking_lot", + "parquet", + "rayon", + "regex", + "serde", + "serde_json", + "strum", + "strum_macros", + "tokio", +] + +[[package]] +name = "tdc-ops" +version = "0.1.0-alpha.1" +dependencies = [ + "arrow-arith", + "arrow-array", + "arrow-cast", + "arrow-ord", + "arrow-schema", + "arrow-select", + "pyo3", + "pyo3-arrow", +] + +[[package]] +name = "tdc-parsers" +version = "0.1.0-alpha.1" +dependencies = [ + "ahash", + "arrow-array", + "arrow-cast", + "arrow-schema", + "chrono", + "criterion", + "fast-float", + "pyo3", + "quick-xml", + "rayon", + "tdc-core", + "tempfile", +] + +[[package]] +name = "tdc-python" +version = "0.1.0-alpha.1" +dependencies = [ + "ahash", + "arrow-array", + "arrow-cast", + "arrow-schema", + "chrono", + "datafusion", + "datafusion-functions", + "datafusion-functions-window", + "futures", + "num_cpus", + "once_cell", + "pyo3", + "pyo3-arrow", + "pyo3-log", + "serde", + "strum", + "tdc-core", + "tdc-ops", + "tdc-parsers", + "tokio", +] + +[[package]] +name = "tempfile" +version = "3.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thrift" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e54bc85fc7faa8bc175c4bab5b92ba8d9a3ce893d0e9f42cc455c8ab16a9e09" +dependencies = [ + "byteorder", + "integer-encoding", + "ordered-float", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", +] + +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d6efd31 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[workspace] +members = [ + "crates/tdc-core", + "crates/tdc-ops", + "crates/tdc-parsers", + "crates/tdc-python", +] +resolver = "2" + +[workspace.package] +version = "0.1.0-alpha.1" +edition = "2024" +license = "MIT" +authors = ["Pieter Robberechts "] +repository = "https://github.com/probberechts/tracking-data-container" + +[profile.release] +codegen-units = 1 +lto = "thin" +strip = true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ab23d2d --- /dev/null +++ b/Makefile @@ -0,0 +1,118 @@ +.DEFAULT_GOAL := all +sources = python/tracking_data_container +test_sources = python/tests + +export CARGO_TERM_COLOR=$(shell (test -t 0 && echo always) || echo auto) + +.PHONY: .uv ## Check that uv is installed +.uv: + @uv -V || echo 'Please install uv: https://docs.astral.sh/uv/getting-started/installation/' + +.PHONY: install ## Install the package & dependencies with debug build +install: .uv + uv sync --frozen --group all + uv run maturin develop --uv -E pyarrow,pandas,polars + +.PHONY: install-prod ## Install the package & dependencies with release build +install-prod: .uv + uv sync --frozen --group all + uv run maturin develop --uv --release -E pyarrow,pandas,polars + +.PHONY: setup-dev ## First-time setup: install + pre-commit hooks +setup-dev: install + uv run pre-commit install --install-hooks + +.PHONY: rebuild-lockfiles ## Rebuild lockfiles from scratch, updating all dependencies +rebuild-lockfiles: .uv + uv lock --upgrade + cargo update + +.PHONY: build-dev ## Build the development version of the package +build-dev: + uv run maturin build + +.PHONY: build-wheel ## Build production wheel and install it +build-wheel: + @rm -rf target/wheels/ + uv run maturin build --release + @wheel=$$(ls target/wheels/*.whl); uv pip install --force-reinstall "$$wheel[pandas,polars]" + +.PHONY: lint-python ## Lint python source files +lint-python: + uv run ruff check $(sources) $(test_sources) + uv run ruff format --check $(sources) $(test_sources) + uv run mypy $(sources) + +.PHONY: lint-rust ## Lint rust source files +lint-rust: + cargo fmt --all -- --check + cargo clippy --workspace --tests -- -D warnings + +.PHONY: lint ## Lint rust and python source files +lint: lint-python lint-rust + +.PHONY: format-python ## Auto-format python source files +format-python: + uv run ruff check --fix $(sources) $(test_sources) + uv run ruff format $(sources) $(test_sources) + +.PHONY: format-rust ## Auto-format rust source files +format-rust: + cargo fmt --all + cargo clippy --workspace --tests --fix --allow-dirty --allow-staged + +.PHONY: format ## Auto-format python and rust source files +format: format-rust format-python + +.PHONY: test-python ## Run python tests +test-python: install + uv run pytest $(sources) $(test_sources) --doctest-modules --ignore=$(test_sources)/benchmarks + +.PHONY: test-rust ## Run rust tests +test-rust: + cargo test --workspace + +.PHONY: test ## Run all tests +test: test-rust test-python + +.PHONY: benchmarks ## Run benchmarks +benchmarks: .uv + uv sync --frozen --group dev --group benchmarks + uv run maturin develop --uv --release -E pyarrow,pandas,polars + uv run pytest python/tests/benchmarks --benchmark-group-by=group --benchmark-warmup=off --benchmark-max-time=2 + +.PHONY: doc-serve ## Serve documentation with live reload +doc-serve: build-dev + uv run pdoc python/tracking_data_container + +.PHONY: doc ## Build documentation +doc: build-dev + uv run pdoc -o docs python/tracking_data_container + cargo doc --no-deps --workspace + +.PHONY: all ## Run the standard set of checks performed in CI +all: format build-dev lint test + +.PHONY: clean ## Clear local caches and build artifacts +clean: + rm -rf `find . -name __pycache__` + rm -f `find . -type f -name '*.py[co]' ` + rm -f `find . -type f -name '*~' ` + rm -f `find . -type f -name '.*~' ` + rm -rf .cache + rm -rf htmlcov + rm -rf .pytest_cache + rm -rf *.egg-info + rm -f .coverage + rm -f .coverage.* + rm -rf build + rm -rf target + rm -rf perf.data* + rm -rf python/tracking_data_container/*.so + +.PHONY: help ## Display this message +help: + @grep -E \ + '^.PHONY: .*?## .*$$' $(MAKEFILE_LIST) | \ + sort | \ + awk 'BEGIN {FS = ".PHONY: |## "}; {printf "\033[36m%-19s\033[0m %s\n", $$2, $$3}' diff --git a/README.md b/README.md new file mode 100644 index 0000000..47b76c9 --- /dev/null +++ b/README.md @@ -0,0 +1,154 @@ +# Tracking Data Container (TDC) + +[![CI](https://github.com/probberechts/tracking-data-container/actions/workflows/CI.yml/badge.svg)](https://github.com/probberechts/tracking-data-container/actions/workflows/CI.yml) +[![PyPI version](https://img.shields.io/pypi/v/tracking-data-container.svg)](https://pypi.org/project/tracking-data-container/) +[![Crates.io](https://img.shields.io/crates/v/tdc-core.svg)](https://crates.io/crates/tdc-core) + +## Summary + +The **TrackingDataContainer (TDC)** is a shared, standardized, metadata-aware container for sports tracking data. Backed by Apache Arrow, it acts as the central interface between tools like Kloppy, Floodlight, BallRadar, and others. + +The container combines high-performance, zero-copy data access with a rich metadata model, enabling efficient workflows. It provides a primary I/O interface for the sports tracking data ecosystem. + +## Motivation + +As tracking pipelines grow across football and other sports, the need for modularity, performance, and inter-package interoperability is critical. Many open-source packages in football analytics currently duplicate functionality across parsing, modeling, and data management. + +TDC addresses several key challenges: + +- **Scalability**: Object-based models (e.g., Kloppy) don't scale well to large tracking datasets. +- **Interoperability**: Packages often use inconsistent schemas and lack a shared representation. +- **Efficiency**: Development resources are wasted on duplicated data parsing logic. +- **Reproducibility**: Derived metrics and transforms are often not easily shareable. +- **Flexibility**: Support is needed for skeleton data, physical data, and multi-team use cases. + +By leveraging Apache Arrow with enforced metadata and a shared schema, TDC solves these problems while maintaining compatibility with modern analytics engines (Polars, DuckDB, etc.). + +## Key Features + +- **Standardized CDF Schema**: Fully aligned with the [Common Data Format (CDF)](https://doi.org/10.48550/arXiv.2401.01882) standard for interoperability. +- **Zero-Copy Interoperability**: Seamlessly move data between TDC, Polars, Pandas, and PyArrow using the Arrow PyCapsule interface. +- **Lazy Evaluation**: Deferred computation and filter pushdown using Polars and DataFusion. +- **Metadata Awareness**: Native support for coordinate systems, pitch dimensions, and orientation normalization (e.g., attacking direction). +- **Frame Materialization**: Efficiently extract and inspect individual frames indexed by time or index. + +## Python Installation + +```bash +pip install tracking_data_container +``` + +## Python Usage + +### Creating and Inspecting Data + +```python +import tracking_data_container as tdc +from datetime import datetime, timezone + +data = { + "frame_id": [1, 2], + "timestamp": [datetime.now(timezone.utc), datetime.now(timezone.utc)], + "ball/x": [10.0, 11.0], + "ball/y": [20.0, 21.0], + "period": ["first_half", "first_half"] +} +container = tdc.TrackingDataContainer(data) + +# Materialize a specific frame +frame = container.materialize_frame(0) +print(frame["ball/x"]) # 10.0 +``` + +### Derived Metrics and Selectors + +```python +# Add a derived column lazily using selectors +lazy_c = container.lazy() +plan = lazy_c.add_column( + "ball/speed", + (tdc.col("ball/x").diff().fill_null(0).pow(2) + + tdc.col("ball/y").diff().fill_null(0).pow(2)).sqrt() +) +result = plan.collect() + +# Select all home player positions +home_players = result.select(result.selector.players(team_id="home")) +``` + +### Coordinate Transformations + +```python +# Scale to a metric coordinate system +# First, define the current coordinate system +current_sys = tdc.CoordinateSystem( + x_range=(0, 100), + y_range=(0, 100), + origin="top_left", + length_unit="percent" +) +container = container.with_coordinate_system(current_sys) + +# Then transform to the target system +metric_sys = tdc.CoordinateSystem( + x_range=(-52.5, 52.5), + y_range=(-34, 34), + origin="center", + length_unit="meters" +) +metric_container = container.transform_coordinates(metric_sys) +``` + +## Project Structure + +- `crates/tdc-core`: Core Rust implementation (Apache Arrow + domain logic). +- `crates/tdc-python`: PyO3 bindings. +- `python/`: High-level Python package. + +## Development + +TDC is a mixed Rust/Python project. We use `uv` for Python dependency management and `maturin` for building the Rust extensions. + +### Prerequisites + +- [Rust](https://www.rust-lang.org/tools/install) (latest stable) +- [uv](https://docs.astral.sh/uv/getting-started/installation/) + +### Setup and Building + +```bash +# First-time setup (installs dependencies and pre-commit hooks) +make setup-dev + +# Build the development version +make build-dev +``` + +### Testing + +```bash +# Run all tests (Rust + Python) +make test + +# Run Rust tests only +make test-rust + +# Run Python tests only +make test-python +``` + +### Linting and Formatting + +We use `ruff` and `mypy` for Python, and `cargo fmt/clippy` for Rust. + +```bash +# Run all linters +make lint + +# Auto-format all code +make format +``` + +## License + +MIT diff --git a/crates/tdc-core/Cargo.toml b/crates/tdc-core/Cargo.toml new file mode 100644 index 0000000..b3740a7 --- /dev/null +++ b/crates/tdc-core/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "tdc-core" +version = "0.1.0-alpha.1" +edition = "2024" +license = "MIT" +description = "Core engine for standardized sports tracking data containers." + +[dependencies] +arrow = { version = "54" } +arrow-array = { version = "54", features = ["ffi"] } +arrow-schema = { version = "54" } +arrow-select = { version = "54" } +arrow-arith = { version = "54" } +arrow-cast = { version = "54" } +arrow-ord = { version = "54" } +datafusion = "45" +chrono = { version = "^0.4.42", default-features = false } +log = "^0.4" +strum = "0.27.2" +strum_macros = "0.27.2" +regex = "1" +tokio = { version = "1", features = ["rt-multi-thread"] } +rayon = "1.10" +serde = { version = "1.0", features = ["derive"] } +ahash = "0.8" +parking_lot = "0.12" +num_cpus = "1.16" +once_cell = "1.19" +parquet = "54" +serde_json = "1.0.149" + +[dev-dependencies] +criterion = { version = "0.5", features = ["html_reports"] } + +[[bench]] +name = "ops" +harness = false + +[features] +default = [] diff --git a/crates/tdc-core/benches/ops.rs b/crates/tdc-core/benches/ops.rs new file mode 100644 index 0000000..fd83150 --- /dev/null +++ b/crates/tdc-core/benches/ops.rs @@ -0,0 +1,151 @@ +use arrow_array::Float64Array; +use arrow_schema::DataType; +use criterion::{Criterion, black_box, criterion_group, criterion_main}; +use datafusion::error::Result as DFResult; +use datafusion::logical_expr::{ColumnarValue, ScalarUDF, ScalarUDFImpl, Signature, Volatility}; +use datafusion::prelude::*; +use std::sync::Arc; +use tdc_core::{ + CoordinateSystem, Direction, LengthUnit, OrientationMode, Origin, TrackingDataBuilder, +}; + +#[derive(Debug)] +struct SpeedUDF { + signature: Signature, +} + +impl SpeedUDF { + fn new() -> Self { + Self { + signature: Signature::exact( + vec![DataType::Float64, DataType::Float64], + Volatility::Immutable, + ), + } + } +} + +impl ScalarUDFImpl for SpeedUDF { + fn as_any(&self) -> &dyn std::any::Any { + self + } + fn name(&self) -> &str { + "speed" + } + fn signature(&self) -> &Signature { + &self.signature + } + fn return_type(&self, _arg_types: &[DataType]) -> DFResult { + Ok(DataType::Float64) + } + fn invoke(&self, args: &[ColumnarValue]) -> DFResult { + let args = ColumnarValue::values_to_arrays(args)?; + let vx = args[0].as_any().downcast_ref::().unwrap(); + let vy = args[1].as_any().downcast_ref::().unwrap(); + + let res: Float64Array = vx + .iter() + .zip(vy.iter()) + .map(|(x, y)| match (x, y) { + (Some(x), Some(y)) => Some((x * x + y * y).sqrt()), + _ => None, + }) + .collect(); + + Ok(ColumnarValue::Array(Arc::new(res))) + } +} + +fn setup_container(num_rows: usize) -> tdc_core::TrackingDataContainer { + let mut builder = TrackingDataBuilder::new(); + let vx_idx = builder.get_or_create_column_id("vx", DataType::Float64); + let vy_idx = builder.get_or_create_column_id("vy", DataType::Float64); + let ball_x_idx = builder.get_or_create_column_id("ball/x", DataType::Float64); + let ball_y_idx = builder.get_or_create_column_id("ball/y", DataType::Float64); + let period_idx = builder.get_or_create_column_id("period", DataType::Utf8); + + for i in 0..num_rows { + let frame_id = i as i64; + let row_idx = builder.get_row_index(frame_id); + builder.insert_string_at_idx( + row_idx, + period_idx, + if i < num_rows / 2 { + "first_half" + } else { + "second_half" + }, + ); + builder.insert_float_at_idx(row_idx, ball_x_idx, i as f64 * 0.1); + builder.insert_float_at_idx(row_idx, ball_y_idx, i as f64 * 0.05); + builder.insert_float_at_idx(row_idx, vx_idx, 1.5); + builder.insert_float_at_idx(row_idx, vy_idx, 2.0); + } + + let container = builder.build().unwrap(); + let source_sys = CoordinateSystem { + x_range: Some((-52.5, 52.5)), + y_range: Some((-34.0, 34.0)), + origin: Origin::Center, + x_direction: Direction::Right, + y_direction: Direction::Up, + length_unit: Some(LengthUnit::Meters), + }; + container + .with_coordinate_system(source_sys) + .with_orientation(OrientationMode::StaticHomeAway) +} + +fn bench_ops(c: &mut Criterion) { + let num_rows = 100_000; + let container = setup_container(num_rows); + let ctx = SessionContext::new(); + let rt = tokio::runtime::Runtime::new().unwrap(); + + // 1. Transform Coordinates + let target_sys = CoordinateSystem { + x_range: Some((0.0, 100.0)), + y_range: Some((0.0, 100.0)), + origin: Origin::TopLeft, + x_direction: Direction::Right, + y_direction: Direction::Down, + length_unit: Some(LengthUnit::Percent), + }; + + c.bench_function("transform_coordinates", |b| { + b.iter(|| { + black_box(&container) + .transform_coordinates(black_box(target_sys.clone())) + .unwrap() + }) + }); + + // 2. Transform Orientation + c.bench_function("transform_orientation", |b| { + b.iter(|| { + black_box(&container) + .transform_orientation(black_box(OrientationMode::HomeAway)) + .unwrap() + }) + }); + + // 3. UDF (Speed) + let speed_udf = ScalarUDF::from(SpeedUDF::new()); + let expr = speed_udf.call(vec![col("vx"), col("vy")]); + + c.bench_function("add_speed_column_udf", |b| { + b.iter(|| { + black_box(&container) + .with_expr_columns_ctx( + vec!["speed".to_string()], + vec![black_box(expr.clone())], + &ctx, + rt.handle(), + ) + .unwrap() + }) + }); +} + +criterion_group!(benches, bench_ops); +criterion_main!(benches); diff --git a/crates/tdc-core/src/config.rs b/crates/tdc-core/src/config.rs new file mode 100644 index 0000000..c6f788a --- /dev/null +++ b/crates/tdc-core/src/config.rs @@ -0,0 +1,27 @@ +use datafusion::prelude::{SessionConfig, SessionContext}; +use once_cell::sync::Lazy; +use tokio::runtime::Runtime; + +pub static RUNTIME: Lazy = Lazy::new(|| { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("Failed to create Tokio runtime") +}); + +pub static CONTEXT: Lazy = Lazy::new(|| { + let config = SessionConfig::new() + .with_target_partitions(num_cpus::get()) + .with_batch_size(16384) + .with_repartition_joins(true) + .with_repartition_aggregations(true) + .with_repartition_windows(true) + .with_repartition_sorts(true) + .with_parquet_pruning(true); + + SessionContext::new_with_config(config) +}); + +pub fn num_cpus() -> usize { + num_cpus::get() +} diff --git a/crates/tdc-core/src/datacontainer/builder.rs b/crates/tdc-core/src/datacontainer/builder.rs new file mode 100644 index 0000000..e4a087c --- /dev/null +++ b/crates/tdc-core/src/datacontainer/builder.rs @@ -0,0 +1,1382 @@ +use crate::error::TrackingDataContainerResult; +use crate::schema::{DataField, Fields, FrameAttribute}; +use ahash::AHashMap; +use arrow::buffer::{BooleanBuffer, MutableBuffer}; +use arrow::util::bit_util; +use arrow_array::{Array, ArrayRef, BooleanArray, Int64Array, RecordBatch, StringArray}; +use arrow_schema::{DataType, Field, Schema}; +use std::sync::Arc; + +/// A simplified builder for a single column that handles random-access insertion. +#[derive(Debug)] +struct TypedColumn { + values: Vec, + validity: MutableBuffer, // Bit-packed validity map + null_value: T, +} + +impl Clone for TypedColumn { + fn clone(&self) -> Self { + let mut validity = MutableBuffer::with_capacity(self.validity.len()); + validity.extend_from_slice(self.validity.as_slice()); + Self { + values: self.values.clone(), + validity, + null_value: self.null_value, + } + } +} + +impl TypedColumn { + fn new(capacity: usize, null_value: T) -> Self { + let mut validity = MutableBuffer::new(capacity.div_ceil(8)); + validity.extend_zeros(capacity.div_ceil(8)); + Self { + values: vec![null_value; capacity], + validity, + null_value, + } + } + + #[inline] + fn len(&self) -> usize { + self.values.len() + } + + #[inline] + fn ensure_capacity(&mut self, target_len: usize) { + if target_len > self.len() { + self.values.resize(target_len, self.null_value); + let new_validity_bytes = target_len.div_ceil(8); + if new_validity_bytes > self.validity.len() { + self.validity + .extend_zeros(new_validity_bytes - self.validity.len()); + } + } + } + + #[inline] + fn insert(&mut self, index: usize, val: T) { + if index >= self.len() { + self.ensure_capacity(index + 1); + } + self.values[index] = val; + bit_util::set_bit(self.validity.as_slice_mut(), index); + } + + fn rebase(&mut self, mapping: &[(usize, usize)], new_total: usize) { + let mut new_values = vec![self.null_value; new_total]; + let mut new_validity = MutableBuffer::new(new_total.div_ceil(8)); + new_validity.extend_zeros(new_total.div_ceil(8)); + + for &(old_idx, new_idx) in mapping { + if old_idx < self.values.len() && bit_util::get_bit(self.validity.as_slice(), old_idx) { + new_values[new_idx] = self.values[old_idx]; + bit_util::set_bit(new_validity.as_slice_mut(), new_idx); + } + } + self.values = new_values; + self.validity = new_validity; + } + + fn merge_fast(&mut self, other: &Self, min_row: usize, max_row: usize) { + self.ensure_capacity(other.len()); + if max_row < min_row || min_row >= other.len() { + return; + } + let end = (max_row + 1).min(other.len()); + + let src_values = &other.values[min_row..end]; + let dst_values = &mut self.values[min_row..end]; + + for i in 0..src_values.len() { + let row = min_row + i; + if bit_util::get_bit(other.validity.as_slice(), row) { + dst_values[i] = src_values[i]; + bit_util::set_bit(self.validity.as_slice_mut(), row); + } + } + } + + fn merge_contiguous(&mut self, other: Self) { + let old_len = self.len(); + let other_len = other.len(); + self.values.extend(other.values); + + let new_total_len = old_len + other_len; + let new_validity_bytes = new_total_len.div_ceil(8); + self.validity.resize(new_validity_bytes, 0); + + for i in 0..other_len { + if bit_util::get_bit(other.validity.as_slice(), i) { + bit_util::set_bit(self.validity.as_slice_mut(), old_len + i); + } + } + } + + fn merge_from(&mut self, other: &Self, mapping: &[(usize, usize)], new_total: usize) { + self.ensure_capacity(new_total); + for &(other_idx, self_idx) in mapping { + if other_idx < other.len() && bit_util::get_bit(other.validity.as_slice(), other_idx) { + self.values[self_idx] = other.values[other_idx]; + bit_util::set_bit(self.validity.as_slice_mut(), self_idx); + } + } + } +} + +#[derive(Debug)] +enum ColumnBuilder { + Int(TypedColumn), + UInt8(TypedColumn), + UInt32(TypedColumn), + Timestamp(TypedColumn), + Float32(TypedColumn), + Float64(TypedColumn), + Bool { + values: MutableBuffer, + validity: MutableBuffer, + len: usize, + }, + String { + values: Vec>, + }, + Dictionary { + values: TypedColumn, + dictionary: Vec, + lookup: AHashMap, + }, +} + +impl Clone for ColumnBuilder { + fn clone(&self) -> Self { + match self { + Self::Int(c) => Self::Int(c.clone()), + Self::UInt8(c) => Self::UInt8(c.clone()), + Self::UInt32(c) => Self::UInt32(c.clone()), + Self::Timestamp(c) => Self::Timestamp(c.clone()), + Self::Float32(c) => Self::Float32(c.clone()), + Self::Float64(c) => Self::Float64(c.clone()), + Self::Bool { + values, + validity, + len, + } => { + let mut v = MutableBuffer::with_capacity(values.len()); + v.extend_from_slice(values.as_slice()); + let mut val = MutableBuffer::with_capacity(validity.len()); + val.extend_from_slice(validity.as_slice()); + Self::Bool { + values: v, + validity: val, + len: *len, + } + } + Self::String { values } => Self::String { + values: values.clone(), + }, + Self::Dictionary { + values, + dictionary, + lookup, + } => Self::Dictionary { + values: values.clone(), + dictionary: dictionary.clone(), + lookup: lookup.clone(), + }, + } + } +} + +impl ColumnBuilder { + fn new(capacity: usize, dtype: DataType) -> Self { + match dtype { + DataType::Int64 | DataType::UInt64 => Self::Int(TypedColumn::new(capacity, 0)), + DataType::UInt8 => Self::UInt8(TypedColumn::new(capacity, 0)), + DataType::UInt32 => Self::UInt32(TypedColumn::new(capacity, 0)), + DataType::Timestamp(_, _) => Self::Timestamp(TypedColumn::new(capacity, 0)), + DataType::Float32 => Self::Float32(TypedColumn::new(capacity, f32::NAN)), + DataType::Float64 => Self::Float64(TypedColumn::new(capacity, f64::NAN)), + DataType::Boolean => { + let bytes = capacity.div_ceil(8); + let mut values = MutableBuffer::new(bytes); + values.extend_zeros(bytes); + let mut validity = MutableBuffer::new(bytes); + validity.extend_zeros(bytes); + Self::Bool { + values, + validity, + len: capacity, + } + } + DataType::Dictionary(_, _) => Self::Dictionary { + values: TypedColumn::new(capacity, 0), + dictionary: Vec::new(), + lookup: AHashMap::new(), + }, + _ => Self::String { + values: vec![None; capacity], + }, + } + } + + fn ensure_capacity(&mut self, target_len: usize) { + match self { + Self::Int(c) => c.ensure_capacity(target_len), + Self::UInt8(c) => c.ensure_capacity(target_len), + Self::UInt32(c) => c.ensure_capacity(target_len), + Self::Timestamp(c) => c.ensure_capacity(target_len), + Self::Float32(c) => c.ensure_capacity(target_len), + Self::Float64(c) => c.ensure_capacity(target_len), + Self::Bool { + values, + validity, + len, + } => { + if target_len > *len { + let bytes = target_len.div_ceil(8); + if bytes > values.len() { + values.extend_zeros(bytes - values.len()); + validity.extend_zeros(bytes - validity.len()); + } + *len = target_len; + } + } + Self::String { values } => values.resize(target_len, None), + Self::Dictionary { values, .. } => values.ensure_capacity(target_len), + } + } + + #[inline] + fn insert_int(&mut self, index: usize, val: i64) { + match self { + Self::Int(c) => c.insert(index, val), + Self::UInt8(c) => c.insert(index, val as u8), + Self::UInt32(c) => c.insert(index, val as u32), + Self::Timestamp(c) => c.insert(index, val), + Self::Float32(_) | Self::Float64(_) => { + self.insert_float(index, val as f64); + } + Self::Dictionary { .. } => self.insert_string(index, &val.to_string()), + _ => { + self.convert_to_string(); + if let Self::String { values } = self { + if index >= values.len() { + values.resize(index + 1, None); + } + values[index] = Some(val.to_string()); + } + } + } + } + + #[inline] + fn insert_float(&mut self, index: usize, val: f64) { + match self { + Self::Float32(c) => c.insert(index, val as f32), + Self::Float64(c) => c.insert(index, val), + Self::Int(_) => { + self.convert_to_float64(); + self.insert_float(index, val); + } + Self::Dictionary { .. } => self.insert_string(index, &val.to_string()), + _ => { + self.convert_to_string(); + if let Self::String { values } = self { + if index >= values.len() { + values.resize(index + 1, None); + } + values[index] = Some(val.to_string()); + } + } + } + } + + #[inline] + fn insert_string(&mut self, index: usize, val: &str) { + match self { + Self::String { values } => { + if index >= values.len() { + values.resize(index + 1, None); + } + values[index] = Some(val.to_string()); + } + Self::Dictionary { + values, + dictionary, + lookup, + } => { + let id = if let Some(&id) = lookup.get(val) { + id + } else { + let s = val.to_string(); + let id = dictionary.len() as u32; + dictionary.push(s.clone()); + lookup.insert(s, id); + id + }; + values.insert(index, id); + } + _ => { + self.convert_to_string(); + if let Self::String { values } = self { + if index >= values.len() { + values.resize(index + 1, None); + } + values[index] = Some(val.to_string()); + } + } + } + } + + #[inline] + fn insert_bool(&mut self, index: usize, val: bool) { + match self { + Self::Bool { + values, + validity, + len, + } => { + if index >= *len { + let target_len = index + 1; + let bytes = target_len.div_ceil(8); + if bytes > values.len() { + values.extend_zeros(bytes - values.len()); + validity.extend_zeros(bytes - validity.len()); + } + *len = target_len; + } + if val { + bit_util::set_bit(values.as_slice_mut(), index); + } else { + bit_util::unset_bit(values.as_slice_mut(), index); + } + bit_util::set_bit(validity.as_slice_mut(), index); + } + _ => { + self.convert_to_string(); + if let Self::String { values } = self { + if index >= values.len() { + values.resize(index + 1, None); + } + values[index] = Some(val.to_string()); + } + } + } + } + + fn convert_to_float64(&mut self) { + match self { + Self::Int(c) => { + let mut new_c = TypedColumn::new(c.len(), f64::NAN); + for i in 0..c.len() { + if bit_util::get_bit(c.validity.as_slice(), i) { + new_c.insert(i, c.values[i] as f64); + } + } + *self = Self::Float64(new_c); + } + Self::Float32(c) => { + let mut new_c = TypedColumn::new(c.len(), f64::NAN); + for i in 0..c.len() { + if bit_util::get_bit(c.validity.as_slice(), i) { + new_c.insert(i, c.values[i] as f64); + } + } + *self = Self::Float64(new_c); + } + _ => {} + } + } + + fn convert_to_string(&mut self) { + let values: Vec> = match self { + Self::Int(c) => (0..c.len()) + .map(|i| { + if bit_util::get_bit(c.validity.as_slice(), i) { + Some(c.values[i].to_string()) + } else { + None + } + }) + .collect(), + Self::UInt8(c) => (0..c.len()) + .map(|i| { + if bit_util::get_bit(c.validity.as_slice(), i) { + Some(c.values[i].to_string()) + } else { + None + } + }) + .collect(), + Self::UInt32(c) => (0..c.len()) + .map(|i| { + if bit_util::get_bit(c.validity.as_slice(), i) { + Some(c.values[i].to_string()) + } else { + None + } + }) + .collect(), + Self::Timestamp(c) => (0..c.len()) + .map(|i| { + if bit_util::get_bit(c.validity.as_slice(), i) { + Some(c.values[i].to_string()) + } else { + None + } + }) + .collect(), + Self::Float32(c) => (0..c.len()) + .map(|i| { + if bit_util::get_bit(c.validity.as_slice(), i) { + Some(c.values[i].to_string()) + } else { + None + } + }) + .collect(), + Self::Float64(c) => (0..c.len()) + .map(|i| { + if bit_util::get_bit(c.validity.as_slice(), i) { + Some(c.values[i].to_string()) + } else { + None + } + }) + .collect(), + Self::Bool { + values, + validity, + len, + } => (0..*len) + .map(|i| { + if bit_util::get_bit(validity.as_slice(), i) { + Some(bit_util::get_bit(values.as_slice(), i).to_string()) + } else { + None + } + }) + .collect(), + Self::Dictionary { + values, dictionary, .. + } => (0..values.len()) + .map(|i| { + if bit_util::get_bit(values.validity.as_slice(), i) { + Some(dictionary[values.values[i] as usize].clone()) + } else { + None + } + }) + .collect(), + Self::String { .. } => return, + }; + *self = Self::String { values }; + } + + fn rebase(&mut self, mapping: &[(usize, usize)], new_total: usize) { + match self { + Self::Int(c) => c.rebase(mapping, new_total), + Self::UInt8(c) => c.rebase(mapping, new_total), + Self::UInt32(c) => c.rebase(mapping, new_total), + Self::Timestamp(c) => c.rebase(mapping, new_total), + Self::Float32(c) => c.rebase(mapping, new_total), + Self::Float64(c) => c.rebase(mapping, new_total), + Self::Bool { + values, + validity, + len, + } => { + let bytes = new_total.div_ceil(8); + let mut new_v = MutableBuffer::new(bytes); + new_v.extend_zeros(bytes); + let mut new_val = MutableBuffer::new(bytes); + new_val.extend_zeros(bytes); + + for &(old_idx, new_idx) in mapping { + if old_idx < *len && bit_util::get_bit(validity.as_slice(), old_idx) { + if bit_util::get_bit(values.as_slice(), old_idx) { + bit_util::set_bit(new_v.as_slice_mut(), new_idx); + } + bit_util::set_bit(new_val.as_slice_mut(), new_idx); + } + } + *values = new_v; + *validity = new_val; + *len = new_total; + } + Self::String { values } => { + let mut new_v = vec![None; new_total]; + for &(old_idx, new_idx) in mapping { + if old_idx < values.len() { + new_v[new_idx] = values[old_idx].take(); + } + } + *values = new_v; + } + Self::Dictionary { values, .. } => { + values.rebase(mapping, new_total); + } + } + } + + fn merge_fast(&mut self, other: ColumnBuilder, min_row: usize, max_row: usize) { + match (self, other) { + (Self::Int(s), ColumnBuilder::Int(o)) => s.merge_fast(&o, min_row, max_row), + (Self::UInt8(s), ColumnBuilder::UInt8(o)) => s.merge_fast(&o, min_row, max_row), + (Self::UInt32(s), ColumnBuilder::UInt32(o)) => s.merge_fast(&o, min_row, max_row), + (Self::Timestamp(s), ColumnBuilder::Timestamp(o)) => s.merge_fast(&o, min_row, max_row), + (Self::Float32(s), ColumnBuilder::Float32(o)) => s.merge_fast(&o, min_row, max_row), + (Self::Float64(s), ColumnBuilder::Float64(o)) => s.merge_fast(&o, min_row, max_row), + ( + Self::Bool { + values: s_v, + validity: s_val, + len: s_l, + }, + ColumnBuilder::Bool { + values: o_v, + validity: o_val, + len: o_l, + }, + ) => { + if o_l > *s_l { + let target_len = o_l; + let bytes = target_len.div_ceil(8); + if bytes > s_v.len() { + s_v.extend_zeros(bytes - s_v.len()); + s_val.extend_zeros(bytes - s_val.len()); + } + *s_l = target_len; + } + let end = (max_row + 1).min(o_l); + for i in min_row..end { + if bit_util::get_bit(o_val.as_slice(), i) { + if bit_util::get_bit(o_v.as_slice(), i) { + bit_util::set_bit(s_v.as_slice_mut(), i); + } else { + bit_util::unset_bit(s_v.as_slice_mut(), i); + } + bit_util::set_bit(s_val.as_slice_mut(), i); + } + } + } + (Self::String { values: s_v }, ColumnBuilder::String { values: o_v }) => { + if o_v.len() > s_v.len() { + s_v.resize(o_v.len(), None); + } + let end = (max_row + 1).min(o_v.len()); + for i in min_row..end { + if o_v[i].is_some() { + s_v[i] = o_v[i].clone(); + } + } + } + ( + Self::Dictionary { + values: s_v, + dictionary: s_d, + lookup: s_l, + }, + ColumnBuilder::Dictionary { + values: o_v, + dictionary: o_d, + lookup: _o_l, + }, + ) => { + s_v.ensure_capacity(o_v.len()); + let end = (max_row + 1).min(o_v.len()); + for i in min_row..end { + if bit_util::get_bit(o_v.validity.as_slice(), i) { + let val = &o_d[o_v.values[i] as usize]; + let new_id = *s_l.entry(val.clone()).or_insert_with(|| { + let id = s_d.len() as u32; + s_d.push(val.clone()); + id + }); + s_v.insert(i, new_id); + } + } + } + (s, o) => { + s.convert_to_string(); + let mut o = o; + o.convert_to_string(); + if let (Self::String { values: s_v }, ColumnBuilder::String { values: o_v }) = + (s, o) + { + if o_v.len() > s_v.len() { + s_v.resize(o_v.len(), None); + } + let end = (max_row + 1).min(o_v.len()); + for i in min_row..end { + if o_v[i].is_some() { + s_v[i] = o_v[i].clone(); + } + } + } + } + } + } + + fn merge_contiguous(&mut self, other: ColumnBuilder) { + match (self, other) { + (Self::Int(s), ColumnBuilder::Int(o)) => s.merge_contiguous(o), + (Self::UInt8(s), ColumnBuilder::UInt8(o)) => s.merge_contiguous(o), + (Self::UInt32(s), ColumnBuilder::UInt32(o)) => s.merge_contiguous(o), + (Self::Timestamp(s), ColumnBuilder::Timestamp(o)) => s.merge_contiguous(o), + (Self::Float32(s), ColumnBuilder::Float32(o)) => s.merge_contiguous(o), + (Self::Float64(s), ColumnBuilder::Float64(o)) => s.merge_contiguous(o), + ( + Self::Bool { + values: s_v, + validity: s_val, + len: s_l, + }, + ColumnBuilder::Bool { + values: o_v, + validity: o_val, + len: o_l, + }, + ) => { + let old_len = *s_l; + let other_len = o_l; + let target_len = old_len + other_len; + let bytes = target_len.div_ceil(8); + if bytes > s_v.len() { + s_v.extend_zeros(bytes - s_v.len()); + s_val.extend_zeros(bytes - s_val.len()); + } + *s_l = target_len; + + for i in 0..other_len { + if bit_util::get_bit(o_val.as_slice(), i) { + if bit_util::get_bit(o_v.as_slice(), i) { + bit_util::set_bit(s_v.as_slice_mut(), old_len + i); + } + bit_util::set_bit(s_val.as_slice_mut(), old_len + i); + } + } + } + (Self::String { values: s_v }, ColumnBuilder::String { values: o_v }) => { + s_v.extend(o_v) + } + ( + Self::Dictionary { + values: s_v, + dictionary: s_d, + lookup: s_l, + }, + ColumnBuilder::Dictionary { + values: o_v, + dictionary: o_d, + lookup: _o_l, + }, + ) => { + let old_len = s_v.len(); + let other_len = o_v.len(); + s_v.ensure_capacity(old_len + other_len); + + for i in 0..other_len { + if bit_util::get_bit(o_v.validity.as_slice(), i) { + let val = &o_d[o_v.values[i] as usize]; + let new_id = *s_l.entry(val.clone()).or_insert_with(|| { + let id = s_d.len() as u32; + s_d.push(val.clone()); + id + }); + s_v.insert(old_len + i, new_id); + } + } + } + (s, o) => { + s.convert_to_string(); + let mut o = o; + o.convert_to_string(); + if let (Self::String { values: s_v }, ColumnBuilder::String { values: o_v }) = + (s, o) + { + s_v.extend(o_v); + } + } + } + } + + fn merge_from(&mut self, other: ColumnBuilder, mapping: &[(usize, usize)], new_total: usize) { + match (self, other) { + (Self::Int(s), ColumnBuilder::Int(o)) => s.merge_from(&o, mapping, new_total), + (Self::UInt8(s), ColumnBuilder::UInt8(o)) => s.merge_from(&o, mapping, new_total), + (Self::UInt32(s), ColumnBuilder::UInt32(o)) => s.merge_from(&o, mapping, new_total), + (Self::Timestamp(s), ColumnBuilder::Timestamp(o)) => { + s.merge_from(&o, mapping, new_total) + } + (Self::Float32(s), ColumnBuilder::Float32(o)) => s.merge_from(&o, mapping, new_total), + (Self::Float64(s), ColumnBuilder::Float64(o)) => s.merge_from(&o, mapping, new_total), + ( + Self::Bool { + values: s_v, + validity: s_val, + len: s_l, + }, + ColumnBuilder::Bool { + values: o_v, + validity: o_val, + len: o_l, + }, + ) => { + if new_total > *s_l { + let target_len = new_total; + let bytes = target_len.div_ceil(8); + if bytes > s_v.len() { + s_v.extend_zeros(bytes - s_v.len()); + s_val.extend_zeros(bytes - s_val.len()); + } + *s_l = target_len; + } + for &(o_idx, s_idx) in mapping { + if o_idx < o_l && bit_util::get_bit(o_val.as_slice(), o_idx) { + if bit_util::get_bit(o_v.as_slice(), o_idx) { + bit_util::set_bit(s_v.as_slice_mut(), s_idx); + } else { + bit_util::unset_bit(s_v.as_slice_mut(), s_idx); + } + bit_util::set_bit(s_val.as_slice_mut(), s_idx); + } + } + } + ( + Self::Dictionary { + values: s_v, + dictionary: s_d, + lookup: s_l, + }, + ColumnBuilder::Dictionary { + values: o_v, + dictionary: o_d, + .. + }, + ) => { + s_v.ensure_capacity(new_total); + for &(o_idx, s_idx) in mapping { + if o_idx < o_v.len() && bit_util::get_bit(o_v.validity.as_slice(), o_idx) { + let val = &o_d[o_v.values[o_idx] as usize]; + let new_id = *s_l.entry(val.clone()).or_insert_with(|| { + let nid = s_d.len() as u32; + s_d.push(val.clone()); + nid + }); + s_v.insert(s_idx, new_id); + } + } + } + (s, o) => { + s.convert_to_string(); + let mut o = o; + o.convert_to_string(); + if let (Self::String { values: s_v }, ColumnBuilder::String { values: o_v }) = + (s, o) + { + s_v.resize(new_total, None); + for &(other_idx, self_idx) in mapping { + if other_idx < o_v.len() + && let Some(v) = &o_v[other_idx] + { + s_v[self_idx] = Some(v.clone()); + } + } + } + } + } + } + + fn finish(self) -> (ArrayRef, DataType) { + match self { + Self::Int(c) => { + let len = c.len(); + ( + Arc::new(Int64Array::new( + c.values.into(), + Some(BooleanBuffer::new(c.validity.into(), 0, len).into()), + )), + DataType::Int64, + ) + } + Self::UInt8(c) => { + use arrow_array::UInt8Array; + let len = c.len(); + ( + Arc::new(UInt8Array::new( + c.values.into(), + Some(BooleanBuffer::new(c.validity.into(), 0, len).into()), + )), + DataType::UInt8, + ) + } + Self::UInt32(c) => { + use arrow_array::UInt32Array; + let len = c.len(); + ( + Arc::new(UInt32Array::new( + c.values.into(), + Some(BooleanBuffer::new(c.validity.into(), 0, len).into()), + )), + DataType::UInt32, + ) + } + Self::Timestamp(c) => { + use arrow_array::PrimitiveArray; + use arrow_array::types::TimestampMicrosecondType; + let len = c.len(); + ( + Arc::new(PrimitiveArray::::new( + c.values.into(), + Some(BooleanBuffer::new(c.validity.into(), 0, len).into()), + )), + DataType::Timestamp(arrow_schema::TimeUnit::Microsecond, None), + ) + } + Self::Float32(c) => { + use arrow_array::Float32Array; + let len = c.len(); + ( + Arc::new(Float32Array::new( + c.values.into(), + Some(BooleanBuffer::new(c.validity.into(), 0, len).into()), + )), + DataType::Float32, + ) + } + Self::Float64(c) => { + use arrow_array::Float64Array; + let len = c.len(); + ( + Arc::new(Float64Array::new( + c.values.into(), + Some(BooleanBuffer::new(c.validity.into(), 0, len).into()), + )), + DataType::Float64, + ) + } + Self::Bool { + values, + validity, + len, + } => ( + Arc::new(BooleanArray::new( + BooleanBuffer::new(values.into(), 0, len), + Some(BooleanBuffer::new(validity.into(), 0, len).into()), + )), + DataType::Boolean, + ), + Self::Dictionary { + values, dictionary, .. + } => { + use arrow_array::{DictionaryArray, UInt32Array}; + let len = values.len(); + let keys = UInt32Array::new( + values.values.into(), + Some(BooleanBuffer::new(values.validity.into(), 0, len).into()), + ); + let values = StringArray::from(dictionary); + let array = DictionaryArray::new(keys, Arc::new(values)); + let dtype = array.data_type().clone(); + (Arc::new(array), dtype) + } + Self::String { values } => (Arc::new(StringArray::from(values)), DataType::Utf8), + } + } + + fn convert_to_match(&mut self, other: &Self) { + match (self, other) { + (Self::Int(_), ColumnBuilder::Int(_)) => (), + (Self::UInt8(_), ColumnBuilder::UInt8(_)) => (), + (Self::UInt32(_), ColumnBuilder::UInt32(_)) => (), + (Self::Timestamp(_), ColumnBuilder::Timestamp(_)) => (), + (Self::Float32(_), ColumnBuilder::Float32(_)) => (), + (Self::Float64(_), ColumnBuilder::Float64(_)) => (), + (Self::Bool { .. }, ColumnBuilder::Bool { .. }) => (), + (Self::String { .. }, ColumnBuilder::String { .. }) => (), + (Self::Dictionary { .. }, ColumnBuilder::Dictionary { .. }) => (), + (s, _) => s.convert_to_string(), + } + } +} + +pub struct TrackingDataBuilder { + frame_map: AHashMap, + last_frame: Option<(i64, usize)>, + columns: Vec, + column_names: Vec, + column_index: AHashMap, + domain_fields: Vec, + next_row_index: usize, + frame_offset: i64, + is_fixed: bool, + min_row_touched: usize, + max_row_touched: usize, +} + +impl Default for TrackingDataBuilder { + fn default() -> Self { + Self::new() + } +} + +impl TrackingDataBuilder { + pub fn new() -> Self { + Self { + frame_map: AHashMap::with_capacity(1000), + last_frame: None, + columns: Vec::with_capacity(100), + column_names: Vec::with_capacity(100), + column_index: AHashMap::with_capacity(100), + domain_fields: Vec::with_capacity(100), + next_row_index: 0, + frame_offset: 0, + is_fixed: false, + min_row_touched: usize::MAX, + max_row_touched: 0, + } + } + + pub fn new_fixed(min_id: i64, max_id: i64) -> Self { + let capacity = (max_id - min_id + 1) as usize; + Self { + frame_map: AHashMap::new(), + last_frame: None, + columns: Vec::with_capacity(100), + column_names: Vec::with_capacity(100), + column_index: AHashMap::with_capacity(100), + domain_fields: Vec::with_capacity(100), + next_row_index: capacity, + frame_offset: min_id, + is_fixed: true, + min_row_touched: usize::MAX, + max_row_touched: 0, + } + } + + #[inline] + pub fn get_row_index(&mut self, frame_id: i64) -> usize { + let idx = if self.is_fixed { + (frame_id - self.frame_offset) as usize + } else { + if let Some((last_id, last_idx)) = self.last_frame + && last_id == frame_id + { + return last_idx; + } + let next_idx = self.next_row_index; + let idx = *self.frame_map.entry(frame_id).or_insert_with(|| { + self.next_row_index += 1; + next_idx + }); + self.last_frame = Some((frame_id, idx)); + idx + }; + if idx < self.min_row_touched { + self.min_row_touched = idx; + } + if idx > self.max_row_touched { + self.max_row_touched = idx; + } + idx + } + + pub fn get_or_create_column_id(&mut self, name: &str, mut dtype: DataType) -> usize { + if let Some(&idx) = self.column_index.get(name) { + return idx; + } + let field = DataField::from_name(name); + if field.is_low_cardinality() && dtype == DataType::Utf8 { + dtype = DataType::Dictionary(Box::new(DataType::UInt32), Box::new(DataType::Utf8)); + } + let idx = self.columns.len(); + self.column_index.insert(name.to_string(), idx); + self.column_names.push(name.to_string()); + self.columns + .push(ColumnBuilder::new(self.next_row_index, dtype)); + self.domain_fields.push(field); + idx + } + + #[inline] + fn update_touched_bounds(&mut self, row_idx: usize) { + if row_idx < self.min_row_touched { + self.min_row_touched = row_idx; + } + if row_idx > self.max_row_touched { + self.max_row_touched = row_idx; + } + } + + #[inline] + pub fn insert_int_at_idx(&mut self, row_idx: usize, col_idx: usize, val: i64) { + self.update_touched_bounds(row_idx); + self.columns[col_idx].insert_int(row_idx, val); + } + #[inline] + pub fn insert_float_at_idx(&mut self, row_idx: usize, col_idx: usize, val: f64) { + self.update_touched_bounds(row_idx); + self.columns[col_idx].insert_float(row_idx, val); + } + #[inline] + pub fn insert_string_at_idx(&mut self, row_idx: usize, col_idx: usize, val: &str) { + self.update_touched_bounds(row_idx); + self.columns[col_idx].insert_string(row_idx, val); + } + #[inline] + pub fn insert_bool_at_idx(&mut self, row_idx: usize, col_idx: usize, val: bool) { + self.update_touched_bounds(row_idx); + self.columns[col_idx].insert_bool(row_idx, val); + } + + #[inline] + pub fn insert_int(&mut self, frame_id: i64, col: &str, val: i64) { + let r = self.get_row_index(frame_id); + let d = DataField::from_name(col).default_arrow_type(); + let c = self.get_or_create_column_id(col, d); + self.columns[c].insert_int(r, val); + } + #[inline] + pub fn insert_float(&mut self, frame_id: i64, col: &str, val: f64) { + let r = self.get_row_index(frame_id); + let d = DataField::from_name(col).default_arrow_type(); + let c = self.get_or_create_column_id(col, d); + self.columns[c].insert_float(r, val); + } + #[inline] + pub fn insert_string(&mut self, frame_id: i64, col: &str, val: &str) { + let r = self.get_row_index(frame_id); + let d = DataField::from_name(col).default_arrow_type(); + let c = self.get_or_create_column_id(col, d); + self.columns[c].insert_string(r, val); + } + #[inline] + pub fn insert_bool(&mut self, frame_id: i64, col: &str, val: bool) { + let r = self.get_row_index(frame_id); + let d = DataField::from_name(col).default_arrow_type(); + let c = self.get_or_create_column_id(col, d); + self.columns[c].insert_bool(r, val); + } + + pub fn merge(&mut self, other: TrackingDataBuilder) { + let mut other = other; + if other.column_index.is_empty() { + return; + } + + let other_min = other.min_row_touched; + let other_max = other.max_row_touched; + + // Contiguous merge (StatsPerform) + if self.is_fixed + && other.is_fixed + && self.frame_offset + self.next_row_index as i64 == other.frame_offset + { + for (name, other_idx) in other.column_index { + let other_col = std::mem::replace( + &mut other.columns[other_idx], + ColumnBuilder::Int(TypedColumn::new(0, 0)), + ); + let other_domain_field = std::mem::replace( + &mut other.domain_fields[other_idx], + DataField::from_name("dummy"), + ); + if let Some(&self_idx) = self.column_index.get(&name) { + self.columns[self_idx].merge_contiguous(other_col); + } else { + let mut new_col = ColumnBuilder::new(self.next_row_index, DataType::Int64); + new_col.convert_to_match(&other_col); + new_col.merge_contiguous(other_col); + self.column_index.insert(name.clone(), self.columns.len()); + self.column_names.push(name.clone()); + self.columns.push(new_col); + self.domain_fields.push(other_domain_field); + } + } + self.next_row_index += other.next_row_index; + if other_min <= other_max { + let mapped_max = other_max + self.next_row_index - other.next_row_index; + if mapped_max > self.max_row_touched { + self.max_row_touched = mapped_max; + } + } + return; + } + + // Fast path (Sportec) + if self.is_fixed + && other.is_fixed + && self.frame_offset == other.frame_offset + && self.next_row_index == other.next_row_index + { + for (name, other_idx) in other.column_index { + let other_col = std::mem::replace( + &mut other.columns[other_idx], + ColumnBuilder::Int(TypedColumn::new(0, 0)), + ); + let other_domain_field = std::mem::replace( + &mut other.domain_fields[other_idx], + DataField::from_name("dummy"), + ); + if let Some(&self_idx) = self.column_index.get(&name) { + if other_min <= other_max { + self.columns[self_idx].merge_fast(other_col, other_min, other_max); + } + } else { + self.column_index.insert(name.clone(), self.columns.len()); + self.column_names.push(name.clone()); + self.columns.push(other_col); + self.domain_fields.push(other_domain_field); + } + } + if other_min <= other_max { + if other_min < self.min_row_touched { + self.min_row_touched = other_min; + } + if other_max > self.max_row_touched { + self.max_row_touched = other_max; + } + } + return; + } + + // --- Middle path: Sorted, non-overlapping frame streams --- + // If both builders have sorted frame_ids and they don't overlap, we can do a very fast + // O(N) merge by simply rebasing both. + // We check this by comparing min/max frame IDs. + let (self_min, self_max) = if self.is_fixed { + ( + self.frame_offset, + self.frame_offset + self.next_row_index as i64 - 1, + ) + } else { + // Check if frame_map is sorted. For simplicity, we only optimize if is_fixed + // or if we can easily determine it's sorted. + (0, -1) // Disable for now if not fixed + }; + + let (other_min, other_max_id) = if other.is_fixed { + ( + other.frame_offset, + other.frame_offset + other.next_row_index as i64 - 1, + ) + } else { + (0, -1) + }; + + if self_min <= self_max + && other_min <= other_max_id + && (self_max < other_min || other_max_id < self_min) + { + let ( + first_builder_offset, + first_builder_len, + second_builder_offset, + second_builder_len, + swap, + ) = if self_max < other_min { + ( + self.frame_offset, + self.next_row_index, + other.frame_offset, + other.next_row_index, + false, + ) + } else { + ( + other.frame_offset, + other.next_row_index, + self.frame_offset, + self.next_row_index, + true, + ) + }; + + let gap = (second_builder_offset - (first_builder_offset + first_builder_len as i64)) + as usize; + let new_total = first_builder_len + gap + second_builder_len; + let new_offset = first_builder_offset; + + if !swap { + // 'self' is first. Just resize and then merge 'other'. + for col in &mut self.columns { + col.rebase( + &(0..self.next_row_index).map(|i| (i, i)).collect::>(), + new_total, + ); + } + let other_mapping: Vec<(usize, usize)> = (0..other.next_row_index) + .map(|i| (i, i + first_builder_len + gap)) + .collect(); + + for (name, other_idx) in other.column_index { + let other_col = std::mem::replace( + &mut other.columns[other_idx], + ColumnBuilder::Int(TypedColumn::new(0, 0)), + ); + let other_domain_field = std::mem::replace( + &mut other.domain_fields[other_idx], + DataField::from_name("dummy"), + ); + if let Some(&self_idx) = self.column_index.get(&name) { + self.columns[self_idx].merge_from(other_col, &other_mapping, new_total); + } else { + let mut new_col = other_col; + new_col.rebase(&other_mapping, new_total); + self.column_index.insert(name.clone(), self.columns.len()); + self.column_names.push(name.clone()); + self.columns.push(new_col); + self.domain_fields.push(other_domain_field); + } + } + self.next_row_index = new_total; + self.frame_offset = new_offset; + self.is_fixed = true; + self.max_row_touched = new_total - 1; + return; + } else { + // 'other' is first. This is more complex because we need to move 'self' data. + // Fall back to slow path for now if swap is needed, or implement it if common. + } + } + + // Slow path (Fallback) + let mut all_frame_ids: Vec = if self.is_fixed { + (0..self.next_row_index) + .map(|i| self.frame_offset + i as i64) + .collect() + } else { + self.frame_map.keys().cloned().collect() + }; + + if other.is_fixed { + for i in 0..other.next_row_index { + all_frame_ids.push(other.frame_offset + i as i64); + } + } else { + for id in other.frame_map.keys() { + all_frame_ids.push(*id); + } + } + + all_frame_ids.sort_unstable(); + all_frame_ids.dedup(); + + let new_total = all_frame_ids.len(); + let mut new_frame_map = AHashMap::with_capacity(new_total); + for (idx, id) in all_frame_ids.iter().enumerate() { + new_frame_map.insert(*id, idx); + } + + let self_mapping: Vec<(usize, usize)> = if self.is_fixed { + (0..self.next_row_index) + .map(|i| { + ( + i, + *new_frame_map.get(&(self.frame_offset + i as i64)).unwrap(), + ) + }) + .collect() + } else { + self.frame_map + .iter() + .map(|(id, old)| (*old, *new_frame_map.get(id).unwrap())) + .collect() + }; + for col in &mut self.columns { + col.rebase(&self_mapping, new_total); + } + + let other_mapping: Vec<(usize, usize)> = if other.is_fixed { + (0..other.next_row_index) + .map(|i| { + ( + i, + *new_frame_map.get(&(other.frame_offset + i as i64)).unwrap(), + ) + }) + .collect() + } else { + other + .frame_map + .iter() + .map(|(id, old)| (*old, *new_frame_map.get(id).unwrap())) + .collect() + }; + + for (name, other_idx) in other.column_index { + let other_col = std::mem::replace( + &mut other.columns[other_idx], + ColumnBuilder::Int(TypedColumn::new(0, 0)), + ); + let other_domain_field = std::mem::replace( + &mut other.domain_fields[other_idx], + DataField::from_name("dummy"), + ); + if let Some(&self_idx) = self.column_index.get(&name) { + self.columns[self_idx].merge_from(other_col, &other_mapping, new_total); + } else { + let mut new_col = other_col; + new_col.rebase(&other_mapping, new_total); + self.column_index.insert(name.clone(), self.columns.len()); + self.column_names.push(name.clone()); + self.columns.push(new_col); + self.domain_fields.push(other_domain_field); + } + } + self.frame_map = new_frame_map; + self.next_row_index = new_total; + self.last_frame = None; + self.min_row_touched = 0; + self.max_row_touched = new_total.saturating_sub(1); + self.is_fixed = false; + } + + pub fn build(self) -> TrackingDataContainerResult { + let total_rows = self.next_row_index; + let mut fields = Vec::with_capacity(self.columns.len() + 1); + let mut arrays = Vec::with_capacity(self.columns.len() + 1); + let mut domain_fields = Vec::with_capacity(self.columns.len() + 1); + + for ((name, mut builder), domain_field) in self + .column_names + .into_iter() + .zip(self.columns.into_iter()) + .zip(self.domain_fields.into_iter()) + { + builder.ensure_capacity(total_rows); + let (array, dtype) = builder.finish(); + fields.push(Field::new(name, dtype, true)); + arrays.push(array); + domain_fields.push(domain_field); + } + + if !self.column_index.contains_key("frame_id") { + let mut frame_id_builder = vec![0i64; total_rows]; + if self.is_fixed { + for (i, val) in frame_id_builder.iter_mut().enumerate().take(total_rows) { + *val = self.frame_offset + i as i64; + } + } else { + for (id, idx) in self.frame_map { + frame_id_builder[idx] = id; + } + } + fields.push(Field::new("frame_id", DataType::Int64, false)); + arrays.push(Arc::new(Int64Array::from(frame_id_builder)) as ArrayRef); + domain_fields.push(DataField::Frame { + attribute: FrameAttribute::FrameId, + }); + } + + let batch = RecordBatch::try_new(Arc::new(Schema::new(fields)), arrays)?; + Ok(super::TrackingDataContainer::new( + batch.into(), + Fields { + fields: domain_fields, + }, + )) + } +} diff --git a/crates/tdc-core/src/datacontainer/core.rs b/crates/tdc-core/src/datacontainer/core.rs new file mode 100644 index 0000000..46d1d08 --- /dev/null +++ b/crates/tdc-core/src/datacontainer/core.rs @@ -0,0 +1,404 @@ +use crate::error::{TrackingDataContainerErrorKind, TrackingDataContainerResult}; +use crate::schema::{CoordinateSystem, DataField, Fields, OrientationMode}; +use arrow_arith::numeric::{add, mul}; +use arrow_array::array::{Array, ArrayRef, Float32Array, Float64Array, StructArray}; +use arrow_array::{RecordBatch, Scalar}; +use arrow_ord::cmp::eq; +use arrow_schema::{DataType::*, Field, Fields as ArrowFields}; +use arrow_select::zip::zip; +use datafusion::prelude::*; +use std::sync::Arc; + +impl TrackingDataContainer { + pub fn new(data: StructArray, schema: Fields) -> Self { + Self { + data, + schema, + coordinate_system: None, + orientation: None, + } + } + + pub fn with_coordinate_system(mut self, system: CoordinateSystem) -> Self { + self.coordinate_system = Some(system); + self + } + + pub fn with_orientation(mut self, mode: OrientationMode) -> Self { + self.orientation = Some(mode); + self + } + + pub fn num_rows(&self) -> usize { + self.data.len() + } + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } + + pub fn validate(&self) -> TrackingDataContainerResult<()> { + self.schema.validate() + } + + pub fn lazy(&self) -> TrackingDataContainerResult { + let batch = RecordBatch::from(self.data.clone()); + let df = crate::config::CONTEXT.read_batch(batch)?; + Ok(super::LazyTrackingDataContainer::new( + df, + self.schema.clone(), + self.coordinate_system.clone(), + self.orientation, + )) + } +} + +impl crate::selector::Select for TrackingDataContainer { + fn select(&self, fields: Vec) -> TrackingDataContainerResult { + let indices = self.schema.get_column_indices(&fields); + + let mut new_arrays = Vec::with_capacity(fields.len()); + let mut new_arrow_fields = Vec::with_capacity(fields.len()); + let mut new_schema_fields = Vec::with_capacity(fields.len()); + + for (field, maybe_idx) in fields.into_iter().zip(indices.into_iter()) { + if let Some(idx) = maybe_idx { + new_arrays.push(self.data.column(idx).clone()); + new_arrow_fields.push(self.data.fields()[idx].clone()); + new_schema_fields.push(field); + } + } + + let new_data = if new_arrays.is_empty() { + StructArray::new_empty_fields(self.num_rows(), None) + } else { + StructArray::try_new(ArrowFields::from(new_arrow_fields), new_arrays, None)? + }; + + Ok(Self { + data: new_data, + schema: Fields { + fields: new_schema_fields, + }, + coordinate_system: self.coordinate_system.clone(), + orientation: self.orientation, + }) + } +} + +impl TrackingDataContainer { + pub fn with_columns(&self, fields: Vec, arrays: Vec) -> Self { + let mut new_fields: Vec> = self.data.fields().iter().cloned().collect(); + let mut new_columns: Vec = self.data.columns().to_vec(); + let mut new_schema_fields = self.schema.fields.clone(); + let mut field_map: std::collections::HashMap = new_fields + .iter() + .enumerate() + .map(|(i, f)| (f.name().clone(), i)) + .collect(); + + for (field, array) in fields.into_iter().zip(arrays.into_iter()) { + let name = field.name(); + if let Some(&i) = field_map.get(&name) { + new_columns[i] = array.clone(); + new_fields[i] = Arc::new(Field::new(&name, array.data_type().clone(), true)); + new_schema_fields[i] = field; + } else { + let new_idx = new_fields.len(); + new_fields.push(Arc::new(Field::new(&name, array.data_type().clone(), true))); + new_columns.push(array); + new_schema_fields.push(field); + field_map.insert(name, new_idx); + } + } + Self { + data: StructArray::new(ArrowFields::from(new_fields), new_columns, None), + schema: Fields { + fields: new_schema_fields, + }, + coordinate_system: self.coordinate_system.clone(), + orientation: self.orientation, + } + } + + pub fn with_expr_columns_ctx( + &self, + names: Vec, + exprs: Vec, + ctx: &SessionContext, + rt: &tokio::runtime::Handle, + ) -> TrackingDataContainerResult { + let mut df = ctx.read_batch(RecordBatch::from(self.data.clone()))?; + let new_names = names.clone(); + for (name, expr) in names.into_iter().zip(exprs.into_iter()) { + df = df.with_column(&name, expr)?; + } + df = df.select_columns(&new_names.iter().map(|s| s.as_str()).collect::>())?; + + let batches = rt.block_on(async { df.collect().await })?; + if batches.is_empty() { + return Ok(self.clone()); + } + let full_batch = arrow_select::concat::concat_batches(&batches[0].schema(), &batches)?; + let fields: Vec = full_batch + .schema() + .fields() + .iter() + .map(|f| DataField::from_name(f.name())) + .collect(); + let mut columns = Vec::new(); + for i in 0..full_batch.num_columns() { + columns.push(full_batch.column(i).clone()); + } + Ok(self.with_columns(fields, columns)) + } + + pub fn transform_coordinates( + &self, + to_system: CoordinateSystem, + ) -> TrackingDataContainerResult { + let from = self.coordinate_system.as_ref().ok_or_else(|| { + TrackingDataContainerErrorKind::InvalidParameters("No coordinate system".to_string()) + })?; + + let mut new_cols = self.data.columns().to_vec(); + let mut new_fields = self.data.fields().iter().cloned().collect::>(); + + // Pre-calculate transformation coefficients and hoist scalars + let (m_x, o_x) = from.get_transform_coeffs(&to_system, true)?; + let (m_y, o_y) = from.get_transform_coeffs(&to_system, false)?; + // For Z, we use the same scale as Y for now, or X. Most sports tracking Z is meters if X/Y are meters. + let (m_z, o_z) = (m_y, o_y); + + let m_x_f64 = Scalar::new(Float64Array::from(vec![m_x])); + let o_x_f64 = Scalar::new(Float64Array::from(vec![o_x])); + let m_y_f64 = Scalar::new(Float64Array::from(vec![m_y])); + let o_y_f64 = Scalar::new(Float64Array::from(vec![o_y])); + let m_z_f64 = Scalar::new(Float64Array::from(vec![m_z])); + let o_z_f64 = Scalar::new(Float64Array::from(vec![o_z])); + + let m_x_f32 = Scalar::new(Float32Array::from(vec![m_x as f32])); + let o_x_f32 = Scalar::new(Float32Array::from(vec![o_x as f32])); + let m_y_f32 = Scalar::new(Float32Array::from(vec![m_y as f32])); + let o_y_f32 = Scalar::new(Float32Array::from(vec![o_y as f32])); + let m_z_f32 = Scalar::new(Float32Array::from(vec![m_z as f32])); + let o_z_f32 = Scalar::new(Float32Array::from(vec![o_z as f32])); + + for (i, field) in self.schema.fields.iter().enumerate() { + if let DataField::TrackedEntity { attribute, .. } = field { + use crate::schema::TrackedEntityAttribute::*; + if matches!(attribute, X | Y | Z) { + let (m_f64, o_f64, m_f32, o_f32) = match attribute { + X => (&m_x_f64, &o_x_f64, &m_x_f32, &o_x_f32), + Y => (&m_y_f64, &o_y_f64, &m_y_f32, &o_y_f32), + Z => (&m_z_f64, &o_z_f64, &m_z_f32, &o_z_f32), + _ => unreachable!(), + }; + + let col = self.data.column(i); + let transformed: ArrayRef = match col.data_type() { + Float32 => { + let multiplied = mul(col, m_f32)?; + add(&multiplied, o_f32)? + } + Float64 => { + let multiplied = mul(col, m_f64)?; + add(&multiplied, o_f64)? + } + _ => { + let arr = arrow_cast::cast(col, &Float64)?; + let multiplied = mul(&arr, m_f64)?; + add(&multiplied, o_f64)? + } + }; + new_cols[i] = transformed.clone(); + new_fields[i] = Arc::new(Field::new( + new_fields[i].name(), + transformed.data_type().clone(), + true, + )); + } + } + } + let new_data = StructArray::try_new( + ArrowFields::from(new_fields), + new_cols, + self.data.nulls().cloned(), + )?; + Ok(Self { + data: new_data, + schema: self.schema.clone(), + coordinate_system: Some(to_system), + orientation: self.orientation, + }) + } + + pub fn transform_orientation( + &self, + mode: OrientationMode, + ) -> TrackingDataContainerResult { + let current = self.orientation.as_ref().ok_or_else(|| { + TrackingDataContainerErrorKind::InvalidParameters("No orientation".to_string()) + })?; + if current == &mode { + return Ok(self.clone()); + } + let p_idx = self + .schema + .fields + .iter() + .position(|f| { + matches!( + f, + DataField::Frame { + attribute: crate::schema::FrameAttribute::Period + } + ) + }) + .ok_or_else(|| { + TrackingDataContainerErrorKind::SchemaError("Missing period column".to_string()) + })?; + let p_col = self.data.column(p_idx); + + let mut new_cols = self.data.columns().to_vec(); + let new_fields = self.data.fields().iter().cloned().collect::>(); + + let poss_team_idx = self.schema.fields.iter().position(|f| { + matches!( + f, + DataField::Frame { + attribute: crate::schema::FrameAttribute::BallPossTeamId + } + ) + }); + + let current_mask = self.get_flip_mask(current, p_col, poss_team_idx); + let target_mask = self.get_flip_mask(&mode, p_col, poss_team_idx); + let change_mask = arrow_arith::boolean::or( + &arrow_arith::boolean::and_not(¤t_mask, &target_mask)?, + &arrow_arith::boolean::and_not(&target_mask, ¤t_mask)?, + )?; + + // Hoist flip scalars + let (m_x, o_x) = self + .coordinate_system + .as_ref() + .map(|cs| cs.get_flip_coeffs(true)) + .unwrap_or((-1.0, 0.0)); + let (m_y, o_y) = self + .coordinate_system + .as_ref() + .map(|cs| cs.get_flip_coeffs(false)) + .unwrap_or((-1.0, 0.0)); + + let m_x_f64 = Scalar::new(Float64Array::from(vec![m_x])); + let o_x_f64 = Scalar::new(Float64Array::from(vec![o_x])); + let m_y_f64 = Scalar::new(Float64Array::from(vec![m_y])); + let o_y_f64 = Scalar::new(Float64Array::from(vec![o_y])); + + let m_x_f32 = Scalar::new(Float32Array::from(vec![m_x as f32])); + let o_x_f32 = Scalar::new(Float32Array::from(vec![o_x as f32])); + let m_y_f32 = Scalar::new(Float32Array::from(vec![m_y as f32])); + let o_y_f32 = Scalar::new(Float32Array::from(vec![o_y as f32])); + + for (i, field) in self.schema.fields.iter().enumerate() { + if let DataField::TrackedEntity { attribute, .. } = field + && matches!( + attribute, + crate::schema::TrackedEntityAttribute::X + | crate::schema::TrackedEntityAttribute::Y + ) + { + let is_x = matches!(attribute, crate::schema::TrackedEntityAttribute::X); + let (m_f64, o_f64, m_f32, o_f32) = if is_x { + (&m_x_f64, &o_x_f64, &m_x_f32, &o_x_f32) + } else { + (&m_y_f64, &o_y_f64, &m_y_f32, &o_y_f32) + }; + + let col = self.data.column(i); + let transformed: ArrayRef = match col.data_type() { + Float32 => { + let multiplied = mul(col, m_f32)?; + let flipped = add(&multiplied, o_f32)?; + zip(&change_mask, &flipped, col)? + } + Float64 => { + let multiplied = mul(col, m_f64)?; + let flipped = add(&multiplied, o_f64)?; + zip(&change_mask, &flipped, col)? + } + _ => col.clone(), + }; + new_cols[i] = transformed; + } + } + let new_data = StructArray::try_new( + ArrowFields::from(new_fields), + new_cols, + self.data.nulls().cloned(), + )?; + Ok(Self { + data: new_data, + schema: self.schema.clone(), + coordinate_system: self.coordinate_system.clone(), + orientation: Some(mode), + }) + } + + fn get_flip_mask( + &self, + mode: &OrientationMode, + p_col: &ArrayRef, + poss_team_idx: Option, + ) -> arrow_array::BooleanArray { + let num_rows = self.num_rows(); + match mode { + OrientationMode::HomeAway => eq( + p_col, + &Scalar::new(arrow_array::StringArray::from(vec!["second_half"])), + ) + .unwrap(), + OrientationMode::AwayHome => eq( + p_col, + &Scalar::new(arrow_array::StringArray::from(vec!["first_half"])), + ) + .unwrap(), + OrientationMode::BallOwningTeam => { + if let Some(idx) = poss_team_idx { + let poss_col = self.data.column(idx); + eq( + poss_col, + &Scalar::new(arrow_array::StringArray::from(vec!["away"])), + ) + .unwrap() + } else { + arrow_array::BooleanArray::new_null(num_rows) + } + } + _ => arrow_array::BooleanArray::new_null(num_rows), + } + } +} + +#[derive(Debug)] +pub struct TrackingDataContainer { + pub data: StructArray, + pub schema: Fields, + pub coordinate_system: Option, + pub orientation: Option, +} +impl Clone for TrackingDataContainer { + fn clone(&self) -> Self { + Self { + data: self.data.clone(), + schema: self.schema.clone(), + coordinate_system: self.coordinate_system.clone(), + orientation: self.orientation, + } + } +} +impl crate::selector::expr::HasSchema for TrackingDataContainer { + fn schema(&self) -> &Fields { + &self.schema + } +} diff --git a/crates/tdc-core/src/datacontainer/lazy.rs b/crates/tdc-core/src/datacontainer/lazy.rs new file mode 100644 index 0000000..297fa02 --- /dev/null +++ b/crates/tdc-core/src/datacontainer/lazy.rs @@ -0,0 +1,341 @@ +use crate::datacontainer::TrackingDataContainer; +use crate::error::TrackingDataContainerResult; +use crate::schema::{CoordinateSystem, DataField, Fields, OrientationMode}; +use datafusion::prelude::*; + +#[derive(Clone)] +pub struct LazyTrackingDataContainer { + pub df: DataFrame, + pub schema: Fields, + pub coordinate_system: Option, + pub orientation: Option, +} + +impl crate::selector::expr::HasSchema for LazyTrackingDataContainer { + fn schema(&self) -> &Fields { + &self.schema + } +} + +impl crate::selector::Select for LazyTrackingDataContainer { + fn select(&self, fields: Vec) -> TrackingDataContainerResult { + let col_names: Vec = fields.iter().map(|f| f.name()).collect(); + let df = self + .df + .clone() + .select_columns(&col_names.iter().map(|s| s.as_str()).collect::>())?; + + Ok(Self { + df, + schema: Fields { fields }, + coordinate_system: self.coordinate_system.clone(), + orientation: self.orientation, + }) + } +} + +impl LazyTrackingDataContainer { + pub fn new( + df: DataFrame, + schema: Fields, + coordinate_system: Option, + orientation: Option, + ) -> Self { + Self { + df, + schema, + coordinate_system, + orientation, + } + } + + pub fn with_orientation(mut self, mode: OrientationMode) -> Self { + self.orientation = Some(mode); + self + } + + pub fn filter( + &self, + predicate: datafusion::prelude::Expr, + ) -> TrackingDataContainerResult { + let df = self.df.clone().filter(predicate)?; + Ok(Self { + df, + schema: self.schema.clone(), + coordinate_system: self.coordinate_system.clone(), + orientation: self.orientation, + }) + } + + pub fn transform_coordinates( + &self, + to_system: CoordinateSystem, + ) -> TrackingDataContainerResult { + let from = self.coordinate_system.as_ref().ok_or_else(|| { + crate::error::TrackingDataContainerErrorKind::InvalidParameters( + "No coordinate system".to_string(), + ) + })?; + + let mut current_df = self.df.clone(); + + for field in self.schema.fields.iter() { + if let DataField::TrackedEntity { attribute, .. } = field { + use crate::schema::TrackedEntityAttribute::*; + if matches!(attribute, X | Y | Z) { + let is_x = matches!(attribute, X); + let (m, o) = from.get_transform_coeffs(&to_system, is_x)?; + + let name = field.name(); + let expr = col(&name) * lit(m) + lit(o); + current_df = current_df.with_column(&name, expr)?; + } + } + } + + Ok(Self { + df: current_df, + schema: self.schema.clone(), + coordinate_system: Some(to_system), + orientation: self.orientation, + }) + } + + pub fn transform_orientation( + &self, + mode: OrientationMode, + ) -> TrackingDataContainerResult { + let current = self.orientation.as_ref().ok_or_else(|| { + crate::error::TrackingDataContainerErrorKind::InvalidParameters( + "No orientation".to_string(), + ) + })?; + if current == &mode { + return Ok(self.clone()); + } + + let p_col_name = self + .schema + .fields + .iter() + .find(|f| { + matches!( + f, + DataField::Frame { + attribute: crate::schema::FrameAttribute::Period + } + ) + }) + .map(|f| f.name()) + .ok_or_else(|| { + crate::error::TrackingDataContainerErrorKind::SchemaError( + "Missing period column".to_string(), + ) + })?; + + let poss_team_col_name = self + .schema + .fields + .iter() + .find(|f| { + matches!( + f, + DataField::Frame { + attribute: crate::schema::FrameAttribute::BallPossTeamId + } + ) + }) + .map(|f| f.name()); + + let mut current_df = self.df.clone(); + + for field in self.schema.fields.iter() { + if let DataField::TrackedEntity { attribute, .. } = field + && matches!( + attribute, + crate::schema::TrackedEntityAttribute::X + | crate::schema::TrackedEntityAttribute::Y + ) + { + let is_x = matches!(attribute, crate::schema::TrackedEntityAttribute::X); + let (m, o) = self + .coordinate_system + .as_ref() + .map(|cs| cs.get_flip_coeffs(is_x)) + .unwrap_or((-1.0, 0.0)); + + let name = field.name(); + + let should_flip_current = + self.get_flip_expr(current, &p_col_name, &poss_team_col_name); + let should_flip_target = + self.get_flip_expr(&mode, &p_col_name, &poss_team_col_name); + + let change_needed = (should_flip_current + .clone() + .and(not(should_flip_target.clone()))) + .or(not(should_flip_current).and(should_flip_target)); + + let flipped_expr = col(&name) * lit(m) + lit(o); + let expr = when(change_needed, flipped_expr).otherwise(col(&name))?; + + current_df = current_df.with_column(&name, expr)?; + } + } + + Ok(Self { + df: current_df, + schema: self.schema.clone(), + coordinate_system: self.coordinate_system.clone(), + orientation: Some(mode), + }) + } + + fn get_flip_expr( + &self, + mode: &OrientationMode, + p_col: &str, + poss_col: &Option, + ) -> Expr { + match mode { + OrientationMode::HomeAway => col(p_col).eq(lit("second_half")), + OrientationMode::AwayHome => col(p_col).eq(lit("first_half")), + OrientationMode::BallOwningTeam => { + if let Some(c) = poss_col { + col(c).eq(lit("away")) + } else { + lit(false) + } + } + _ => lit(false), + } + } + + pub fn add_column( + &self, + name: &str, + expr: datafusion::prelude::Expr, + ) -> TrackingDataContainerResult { + self.with_expr_column(name, expr) + } + + pub fn add_columns( + &self, + names: Vec, + exprs: Vec, + ) -> TrackingDataContainerResult { + let mut df = self.df.clone(); + let mut new_schema_fields = self.schema.fields.clone(); + + for (name, expr) in names.into_iter().zip(exprs.into_iter()) { + df = df.with_column(&name, expr)?; + let field = DataField::from_name(&name); + let mut found = false; + for existing_field in &mut new_schema_fields { + if existing_field.name() == name { + *existing_field = field.clone(); + found = true; + break; + } + } + if !found { + new_schema_fields.push(field); + } + } + + Ok(Self { + df, + schema: Fields { + fields: new_schema_fields, + }, + coordinate_system: self.coordinate_system.clone(), + orientation: self.orientation, + }) + } + + pub fn add_array_column( + &self, + _name: &str, + _array: arrow_array::ArrayRef, + ) -> TrackingDataContainerResult { + Err(datafusion::error::DataFusionError::NotImplemented("Adding raw arrays to a lazy container is not yet supported. Use the eager container for direct array additions.".to_string()).into()) + } + + pub fn add_column_udf( + &self, + _name: &str, + _func: std::sync::Arc, + _on: Vec, + ) -> TrackingDataContainerResult { + Err(datafusion::error::DataFusionError::NotImplemented( + "Lazy Python UDFs not yet supported".to_string(), + ) + .into()) + } + + pub fn with_expr_column( + &self, + name: &str, + expr: datafusion::prelude::Expr, + ) -> TrackingDataContainerResult { + let df = self.df.clone().with_column(name, expr)?; + let mut new_schema_fields = self.schema.fields.clone(); + + // Update schema + let field = DataField::from_name(name); + let mut found = false; + for existing_field in &mut new_schema_fields { + if existing_field.name() == name { + *existing_field = field.clone(); + found = true; + break; + } + } + if !found { + new_schema_fields.push(field); + } + + Ok(Self { + df, + schema: Fields { + fields: new_schema_fields, + }, + coordinate_system: self.coordinate_system.clone(), + orientation: self.orientation, + }) + } + + pub async fn collect(self) -> TrackingDataContainerResult { + let batches = self.df.collect().await?; + if batches.is_empty() { + let mut container = TrackingDataContainer::new( + arrow_array::StructArray::new_empty_fields(0, None), + self.schema, + ); + if let Some(cs) = self.coordinate_system { + container = container.with_coordinate_system(cs); + } + if let Some(om) = self.orientation { + container = container.with_orientation(om); + } + return Ok(container); + } + + let batch = if batches.len() == 1 { + batches[0].clone() + } else { + let schema = batches[0].schema(); + arrow_select::concat::concat_batches(&schema, &batches)? + }; + + let mut container = TrackingDataContainer::new(batch.into(), self.schema); + if let Some(cs) = self.coordinate_system { + container = container.with_coordinate_system(cs); + } + if let Some(om) = self.orientation { + container = container.with_orientation(om); + } + + Ok(container) + } +} diff --git a/crates/tdc-core/src/datacontainer/mod.rs b/crates/tdc-core/src/datacontainer/mod.rs new file mode 100644 index 0000000..5cb809f --- /dev/null +++ b/crates/tdc-core/src/datacontainer/mod.rs @@ -0,0 +1,7 @@ +pub mod builder; +pub mod core; +pub mod lazy; + +pub use builder::TrackingDataBuilder; +pub use core::TrackingDataContainer; +pub use lazy::LazyTrackingDataContainer; diff --git a/crates/tdc-core/src/error.rs b/crates/tdc-core/src/error.rs new file mode 100644 index 0000000..eb9fcfb --- /dev/null +++ b/crates/tdc-core/src/error.rs @@ -0,0 +1,147 @@ +use std::{error::Error, fmt::Display}; + +/// The kind of a tracking data container error. +#[derive(Debug)] +pub enum TrackingDataContainerErrorKind { + InvalidParameters(String), + SchemaError(String), + SchemaWarning(String), + ArrowError(arrow::error::ArrowError), + DataFusionError(datafusion::error::DataFusionError), + ParquetError(parquet::errors::ParquetError), + IOError(std::io::Error), + Internal(String), +} + +impl Display for TrackingDataContainerErrorKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TrackingDataContainerErrorKind::InvalidParameters(err) => { + write!(f, "invalid parameters: {err}") + } + TrackingDataContainerErrorKind::SchemaError(err) => { + write!(f, "schema error: {err}") + } + TrackingDataContainerErrorKind::SchemaWarning(warn) => { + write!(f, "schema warning: {warn}") + } + TrackingDataContainerErrorKind::ArrowError(err) => { + write!(f, "arrow error: {err}") + } + TrackingDataContainerErrorKind::DataFusionError(err) => { + write!(f, "datafusion error: {err}") + } + TrackingDataContainerErrorKind::ParquetError(err) => { + write!(f, "parquet error: {err}") + } + TrackingDataContainerErrorKind::IOError(err) => { + write!(f, "io error: {err}") + } + TrackingDataContainerErrorKind::Internal(err) => { + write!(f, "internal error: {err}") + } + } + } +} + +/// A `tracking data container` error. +/// +/// Contains a kind and a context. Use the `Display` trait to format the +/// error message with its context. +#[derive(Debug)] +pub struct TrackingDataContainerError { + pub kind: TrackingDataContainerErrorKind, + pub context: Vec, +} + +pub trait ErrorContext { + fn with_context(self, ctx_fn: F) -> Self + where + F: FnOnce() -> S; +} + +impl TrackingDataContainerError { + pub fn new(kind: TrackingDataContainerErrorKind) -> Self { + Self { + kind, + context: vec![], + } + } +} + +impl Display for TrackingDataContainerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{kind}", kind = self.kind)?; + if !self.context.is_empty() { + writeln!(f, "\nContext:")?; + + self.context + .iter() + .enumerate() + .try_for_each(|(idx, ctx_value)| writeln!(f, " {idx}: {ctx_value}"))?; + } + Ok(()) + } +} + +impl Error for TrackingDataContainerError {} + +impl ErrorContext for TrackingDataContainerError { + fn with_context(mut self, ctx_fn: F) -> Self + where + F: FnOnce() -> S, + { + self.context.push(ctx_fn().to_string()); + self + } +} + +impl From for TrackingDataContainerError { + fn from(kind: TrackingDataContainerErrorKind) -> Self { + TrackingDataContainerError::new(kind) + } +} + +impl From for TrackingDataContainerError { + fn from(err: arrow::error::ArrowError) -> Self { + TrackingDataContainerErrorKind::ArrowError(err).into() + } +} + +impl From for TrackingDataContainerError { + fn from(err: datafusion::error::DataFusionError) -> Self { + TrackingDataContainerErrorKind::DataFusionError(err).into() + } +} + +impl From for TrackingDataContainerError { + fn from(err: parquet::errors::ParquetError) -> Self { + TrackingDataContainerErrorKind::ParquetError(err).into() + } +} + +impl From for TrackingDataContainerError { + fn from(err: std::io::Error) -> Self { + TrackingDataContainerErrorKind::IOError(err).into() + } +} + +impl From for TrackingDataContainerError { + fn from(err: String) -> Self { + TrackingDataContainerErrorKind::Internal(err).into() + } +} + +pub type TrackingDataContainerResult = Result; + +impl ErrorContext for TrackingDataContainerResult { + fn with_context(self, ctx_fn: F) -> Self + where + F: FnOnce() -> S, + { + match self { + Ok(_) => self, + Err(e) => Err(e.with_context(ctx_fn)), + } + } +} diff --git a/crates/tdc-core/src/io.rs b/crates/tdc-core/src/io.rs new file mode 100644 index 0000000..723946b --- /dev/null +++ b/crates/tdc-core/src/io.rs @@ -0,0 +1,91 @@ +use crate::datacontainer::TrackingDataContainer; +use crate::error::TrackingDataContainerResult; +use crate::schema::Fields; +use arrow_array::RecordBatch; +use parquet::arrow::ArrowWriter; +use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder; +use parquet::file::properties::WriterProperties; +use std::fs::File; + +impl TrackingDataContainer { + /// Saves the container to a Parquet file. + pub fn save_parquet(&self, path: &str) -> TrackingDataContainerResult<()> { + let file = File::create(path)?; + let batch = RecordBatch::from(self.data.clone()); + + let mut key_value_metadata = Vec::new(); + + if let Some(cs) = &self.coordinate_system { + let cs_json = serde_json::to_string(cs).map_err(|e| e.to_string())?; + key_value_metadata.push(parquet::format::KeyValue::new( + "tdc:coordinate_system".to_string(), + Some(cs_json), + )); + } + + if let Some(om) = &self.orientation { + let om_json = serde_json::to_string(om).map_err(|e| e.to_string())?; + key_value_metadata.push(parquet::format::KeyValue::new( + "tdc:orientation_mode".to_string(), + Some(om_json), + )); + } + + let props_builder = + WriterProperties::builder().set_key_value_metadata(Some(key_value_metadata)); + + let mut writer = ArrowWriter::try_new(file, batch.schema(), Some(props_builder.build()))?; + writer.write(&batch)?; + writer.close()?; + + Ok(()) + } + + /// Loads a container from a Parquet file. + pub fn load_parquet(path: &str) -> TrackingDataContainerResult { + let file = File::open(path)?; + let builder = ParquetRecordBatchReaderBuilder::try_new(file)?; + + let mut coordinate_system = None; + let mut orientation = None; + + if let Some(metadata) = builder.metadata().file_metadata().key_value_metadata() { + for kv in metadata { + if kv.key == "tdc:coordinate_system" { + if let Some(val) = &kv.value { + coordinate_system = + Some(serde_json::from_str(val).map_err(|e| e.to_string())?); + } + } else if kv.key == "tdc:orientation_mode" + && let Some(val) = &kv.value + { + orientation = Some(serde_json::from_str(val).map_err(|e| e.to_string())?); + } + } + } + + let mut reader = builder.build()?; + let batch_result = reader.next(); + let batch = match batch_result { + Some(Ok(b)) => b, + Some(Err(e)) => return Err(e.into()), + None => return Err("Empty parquet file".to_string().into()), + }; + + let schema_fields: Vec<_> = batch + .schema() + .fields() + .iter() + .map(|f| crate::schema::DataField::from_name(f.name())) + .collect(); + let schema = Fields { + fields: schema_fields, + }; + + let mut container = Self::new(batch.into(), schema); + container.coordinate_system = coordinate_system; + container.orientation = orientation; + + Ok(container) + } +} diff --git a/crates/tdc-core/src/lib.rs b/crates/tdc-core/src/lib.rs new file mode 100644 index 0000000..3ddd353 --- /dev/null +++ b/crates/tdc-core/src/lib.rs @@ -0,0 +1,13 @@ +pub mod config; +pub mod datacontainer; +pub mod error; +pub mod io; +pub mod schema; +pub mod selector; + +pub use datacontainer::{LazyTrackingDataContainer, TrackingDataBuilder, TrackingDataContainer}; +pub use schema::{ + Attribute, CoordinateSystem, DataField, Direction, Fields, FrameAttribute, LengthUnit, + OrientationMode, Origin, Period, TrackedEntity, TrackedEntityAttribute, +}; +pub use selector::Select; diff --git a/crates/tdc-core/src/schema/attribute.rs b/crates/tdc-core/src/schema/attribute.rs new file mode 100644 index 0000000..c407a44 --- /dev/null +++ b/crates/tdc-core/src/schema/attribute.rs @@ -0,0 +1,156 @@ +//! Definitions for frame and entity attributes. +//! +//! This module provides traits and enums for representing various data points captured +//! in sports tracking, such as spatial coordinates, velocity, and match metadata. + +use arrow_schema::{DataType, TimeUnit}; +use strum_macros::{AsRefStr, Display, EnumString}; + +/// Common trait for all schema attributes. +pub trait Attribute { + /// Returns the standardized string representation of the attribute. + fn name(&self) -> &str; + + /// Returns the default Apache Arrow data type used to represent this attribute in memory. + fn default_arrow_type(&self) -> DataType; + + /// Returns true if this attribute typically has low cardinality (few unique values). + fn is_low_cardinality(&self) -> bool; +} + +/// Attributes specifically associated with a tracked entity (Player, Ball, or Referee). +/// +/// These attributes represent spatial and physical data points captured for an entity +/// during a match. +#[derive(Clone, Debug, PartialEq, Eq, Hash, EnumString, AsRefStr, Display)] +#[strum(serialize_all = "snake_case")] +pub enum TrackedEntityAttribute { + /// Unique identifier for the entity (e.g., person ID or ball ID). + Id, + /// X-coordinate on the pitch. + X, + /// Y-coordinate on the pitch. + Y, + /// Z-coordinate (altitude/height) on the pitch. + Z, + /// Distance traveled since the last capture point. + Dist, + /// Instantaneous velocity or speed. + Vel, + /// Instantaneous acceleration. + Acc, + /// Latitude (GPS). + Lat, + /// Longitude (GPS). + Long, + /// Whether the entity was visible to the tracking system at this frame. + IsVisible, + /// Cumulative minutes played by the entity. + MinutesPlayed, + + // Team specific + /// Official name of the team or entity. + Name, + /// Jersey color representation (e.g., hex code or name). + JerseyColour, + /// Tactical formation (e.g., "4-3-3"). + Formation, + + /// Catch-all for custom attributes not defined in the core schema (e.g., "heart_rate"). + Other(String), +} + +impl Attribute for TrackedEntityAttribute { + fn name(&self) -> &str { + match self { + Self::Other(s) => s, + _ => self.as_ref(), + } + } + + /// Returns the default Arrow column type for this attribute. + fn default_arrow_type(&self) -> DataType { + use TrackedEntityAttribute::*; + match self { + Id | Other(_) | Name | JerseyColour | Formation => DataType::Utf8, + IsVisible => DataType::Boolean, + X | Y | Z | Dist | Vel | Acc | Lat | Long | MinutesPlayed => DataType::Float32, + } + } + + fn is_low_cardinality(&self) -> bool { + use TrackedEntityAttribute::*; + matches!(self, Formation | JerseyColour) + } +} + +// ----------------------------------------------------------------------------- +// FrameAttribute +// ----------------------------------------------------------------------------- + +/// Attributes associated with a global frame or the match state. +/// +/// These attributes provide context for the entire set of positions captured +/// at a single point in time. +#[derive(Clone, Debug, PartialEq, Eq, Hash, EnumString, AsRefStr, Display)] +#[strum(serialize_all = "snake_case")] +pub enum FrameAttribute { + /// Unique sequence number for the frame. + FrameId, + /// Match period (e.g., 1 for first half, 2 for second half). + Period, + /// Wall-clock timestamp of the capture. + Timestamp, + + /// Unique identifier for the match. + /// + /// "match" is a reserved keyword in Rust, so MatchId is used as an alternative. + /// The strum attribute ensures it parses from the string "match" correctly. + #[strum(serialize = "match")] + MatchId, + + /// Global ball status (e.g., "alive" or "dead"). + BallStatus, + /// Identifier of the team currently in possession of the ball. + BallPossTeamId, + /// Status of ball possession (e.g., "home", "away", "none"). + BallPossStatus, + + /// Name of the data vendor's event stream. + VendorEventName, + /// Name of the data vendor's tracking system. + VendorTrackingName, + + /// Catch-all for custom frame-level metadata. + Other(String), +} + +impl Attribute for FrameAttribute { + fn name(&self) -> &str { + match self { + Self::Other(s) => s, + _ => self.as_ref(), + } + } + + /// Returns the default Arrow column type for this attribute. + fn default_arrow_type(&self) -> DataType { + use FrameAttribute::*; + match self { + FrameId => DataType::UInt64, + Period => DataType::Utf8, + Timestamp => DataType::Timestamp(TimeUnit::Microsecond, None), + BallStatus => DataType::Boolean, + MatchId | BallPossTeamId | BallPossStatus | VendorEventName | VendorTrackingName + | Other(_) => DataType::Utf8, + } + } + + fn is_low_cardinality(&self) -> bool { + use FrameAttribute::*; + matches!( + self, + Period | BallStatus | BallPossStatus | VendorEventName | VendorTrackingName + ) + } +} diff --git a/crates/tdc-core/src/schema/coordinate_system.rs b/crates/tdc-core/src/schema/coordinate_system.rs new file mode 100644 index 0000000..f21fbe1 --- /dev/null +++ b/crates/tdc-core/src/schema/coordinate_system.rs @@ -0,0 +1,216 @@ +//! Models for spatial metadata and coordinate system definitions. +//! +//! This module provides the tools to describe how coordinates are mapped to the +//! physical world, enabling transformations between different providers and standards. + +use crate::error::{TrackingDataContainerErrorKind, TrackingDataContainerResult}; +use serde::{Deserialize, Serialize}; +use strum_macros::{AsRefStr, Display, EnumString}; + +/// Defines a coordinate system for tracking data. +/// +/// A coordinate system combines spatial bounds (ranges), an origin point, axis orientations, +/// and measurement units to provide a complete context for entity positions. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct CoordinateSystem { + /// The valid range of X coordinates (e.g., Some((-52.5, 52.5))). + pub x_range: Option<(f64, f64)>, + /// The valid range of Y coordinates (e.g., Some((-34.0, 34.0))). + pub y_range: Option<(f64, f64)>, + /// The location of the (0,0) point. + pub origin: Origin, + /// The direction in which X values increase. + pub x_direction: Direction, + /// The direction in which Y values increase. + pub y_direction: Direction, + /// The physical unit of length for the coordinates. + pub length_unit: Option, +} + +/// The location of the origin (0,0) in a coordinate system. +#[derive( + Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, EnumString, AsRefStr, Display, +)] +#[strum(serialize_all = "snake_case")] +pub enum Origin { + /// The center of the pitch/court. + Center, + /// The top-left corner of the pitch (often used in computer vision). + TopLeft, + /// The bottom-left corner of the pitch. + BottomLeft, +} + +/// Possible axis directions. +#[derive( + Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, EnumString, AsRefStr, Display, +)] +#[strum(serialize_all = "snake_case")] +pub enum Direction { + /// Values increase towards the right. + Right, + /// Values increase towards the left. + Left, + /// Values increase towards the top. + Up, + /// Values increase towards the bottom. + Down, +} + +/// Standardized units of length. +#[derive( + Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, EnumString, AsRefStr, Display, +)] +#[strum(serialize_all = "snake_case")] +pub enum LengthUnit { + /// Standard metric meter. + Meters, + /// Centimeter. + Centimeters, + /// Millimeter. + Millimeters, + /// Yard (approx 0.9144 meters). + Yards, + /// Inch. + Inches, + /// Foot. + Feet, + /// Normalized unit where coordinates represent a percentage of pitch dimensions. + Percent, +} + +/// Logic for normalizing playing direction across periods or possession. +#[derive( + Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, EnumString, AsRefStr, Display, +)] +#[strum(serialize_all = "snake_case")] +pub enum OrientationMode { + /// Coordinates are flipped so the team in possession always attacks from left to right. + BallOwningTeam, + /// The home team attacks left-to-right in the first half and right-to-left in the second. + HomeAway, + /// The away team attacks left-to-right in the first half and right-to-left in the second. + AwayHome, + /// The home team always attacks from left to right, regardless of period. + StaticHomeAway, + /// The away team always attacks from left to right, regardless of period. + StaticAwayHome, +} + +impl CoordinateSystem { + /// Creates a new coordinate system definition. + pub fn new( + x_range: Option<(f64, f64)>, + y_range: Option<(f64, f64)>, + origin: Origin, + x_direction: Direction, + y_direction: Direction, + length_unit: Option, + ) -> Self { + Self { + x_range, + y_range, + origin, + x_direction, + y_direction, + length_unit, + } + } +} + +impl CoordinateSystem { + pub fn get_transform_coeffs( + &self, + to: &CoordinateSystem, + is_x: bool, + ) -> TrackingDataContainerResult<(f64, f64)> { + let (f_min, f_max) = if is_x { self.x_range } else { self.y_range }.ok_or_else(|| { + TrackingDataContainerErrorKind::InvalidParameters( + if is_x { + "Source X range undefined" + } else { + "Source Y range undefined" + } + .to_string(), + ) + })?; + let (t_min, t_max) = if is_x { to.x_range } else { to.y_range }.ok_or_else(|| { + TrackingDataContainerErrorKind::InvalidParameters( + if is_x { + "Target X range undefined" + } else { + "Target Y range undefined" + } + .to_string(), + ) + })?; + + let f_scale = f_max - f_min; + let t_scale = t_max - t_min; + + let f_unit = self.length_unit.unwrap_or(LengthUnit::Meters).multiplier(); + let t_unit = to.length_unit.unwrap_or(LengthUnit::Meters).multiplier(); + + let f_sign = if is_x { + if self.x_direction == Direction::Right { + 1.0 + } else { + -1.0 + } + } else if self.y_direction == Direction::Up { + 1.0 + } else { + -1.0 + }; + let t_sign = if is_x { + if to.x_direction == Direction::Right { + 1.0 + } else { + -1.0 + } + } else if to.y_direction == Direction::Up { + 1.0 + } else { + -1.0 + }; + + // Point mapping: physical center maps to physical center. + let f_center_coord = if self.origin == Origin::Center { + 0.0 + } else { + (f_min + f_max) / 2.0 + }; + let t_center_coord = if to.origin == Origin::Center { + 0.0 + } else { + (t_min + t_max) / 2.0 + }; + + let mul = (t_scale / f_scale) * (f_unit / t_unit) * f_sign * t_sign; + let off = t_center_coord - f_center_coord * mul; + + Ok((mul, off)) + } + + /// Returns the multiplier and offset to flip a coordinate across the center of this system. + pub fn get_flip_coeffs(&self, is_x: bool) -> (f64, f64) { + let (min, max) = if is_x { self.x_range } else { self.y_range }.unwrap_or((-50.0, 50.0)); + let center = (min + max) / 2.0; + // f(v) = center - (v - center) = 2*center - v + (-1.0, 2.0 * center) + } +} + +impl LengthUnit { + pub fn multiplier(&self) -> f64 { + match self { + LengthUnit::Meters => 1.0, + LengthUnit::Centimeters => 0.01, + LengthUnit::Millimeters => 0.001, + LengthUnit::Yards => 0.9144, + LengthUnit::Feet => 0.3048, + LengthUnit::Inches => 0.0254, + LengthUnit::Percent => 1.0, + } + } +} diff --git a/crates/tdc-core/src/schema/entity.rs b/crates/tdc-core/src/schema/entity.rs new file mode 100644 index 0000000..76d4b36 --- /dev/null +++ b/crates/tdc-core/src/schema/entity.rs @@ -0,0 +1,28 @@ +//! Definitions for tracked entities. + +/// Represents an entity on the pitch/court. +/// +/// Tracked entities are the primary subjects of the tracking data, providing the context +/// for spatial coordinates and other time-series attributes. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum TrackedEntity { + /// The match ball. + Ball, + /// A player belonging to a specific team. + Player { + /// The unique identifier for the team. + team_id: String, + /// The unique identifier for the player. + player_id: String, + }, + /// A match official. + Referee { + /// The unique identifier for the referee. + referee_id: String, + }, + /// Represents team-level data (e.g., formation or possession) not tied to a specific player. + Team { + /// The unique identifier for the team. + team_id: String, + }, +} diff --git a/crates/tdc-core/src/schema/field.rs b/crates/tdc-core/src/schema/field.rs new file mode 100644 index 0000000..2cee717 --- /dev/null +++ b/crates/tdc-core/src/schema/field.rs @@ -0,0 +1,150 @@ +//! Logic for identifying and parsing individual data columns. + +use super::{Attribute, FrameAttribute, TrackedEntity, TrackedEntityAttribute}; +use arrow_schema::DataType; +use std::fmt; +use std::str::FromStr; + +/// Represents a single column (field) in a tracking dataset. +/// +/// A `DataField` maps an Arrow column name to its domain-specific meaning, +/// such as a player's X-coordinate or a match period. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum DataField { + /// A global attribute for the entire frame. + Frame { + /// The specific frame-level attribute. + attribute: FrameAttribute, + }, + /// An attribute tied to a specific tracked entity. + TrackedEntity { + /// The entity this field belongs to. + entity: TrackedEntity, + /// The specific attribute being tracked. + attribute: TrackedEntityAttribute, + }, +} + +impl DataField { + /// Serializes the DataField back into its standardized hierarchical string representation. + /// + /// # Examples + /// + /// - Player coordinate: `teams/home/players/10/x` + /// - Ball coordinate: `ball/z` + /// - Global metadata: `frame_id` + pub fn name(&self) -> String { + match self { + DataField::Frame { attribute } => match attribute { + FrameAttribute::VendorEventName => "vendor/event/name".to_string(), + FrameAttribute::VendorTrackingName => "vendor/tracking/name".to_string(), + FrameAttribute::MatchId => "match/id".to_string(), + _ => attribute.name().to_string(), + }, + DataField::TrackedEntity { entity, attribute } => { + let attr_name = attribute.name(); + match entity { + TrackedEntity::Ball => format!("ball/{attr_name}"), + TrackedEntity::Player { team_id, player_id } => { + format!("teams/{team_id}/players/{player_id}/{attr_name}") + } + TrackedEntity::Referee { referee_id } => { + format!("referees/{referee_id}/{attr_name}") + } + TrackedEntity::Team { team_id } => { + format!("teams/{team_id}/{attr_name}") + } + } + } + } + } + + /// Parses a hierarchical column name path into a domain-specific `DataField`. + /// + /// This method implements the inverse of [`DataField::name`]. + pub fn from_name(path: &str) -> Self { + let parts: Vec<&str> = path.split('/').collect(); + + match parts.as_slice() { + ["teams", tid, "players", pid, ..] => { + let attr_str = parts[4..].join("/"); + DataField::TrackedEntity { + entity: TrackedEntity::Player { + team_id: tid.to_string(), + player_id: pid.to_string(), + }, + attribute: Self::parse_attr(&attr_str), + } + } + ["teams", tid, ..] => { + let attr_str = parts[2..].join("/"); + DataField::TrackedEntity { + entity: TrackedEntity::Team { + team_id: tid.to_string(), + }, + attribute: Self::parse_attr(&attr_str), + } + } + ["ball", ..] => { + let attr_str = parts[1..].join("/"); + DataField::TrackedEntity { + entity: TrackedEntity::Ball, + attribute: Self::parse_attr(&attr_str), + } + } + ["referees", rid, ..] => { + let attr_str = parts[2..].join("/"); + DataField::TrackedEntity { + entity: TrackedEntity::Referee { + referee_id: rid.to_string(), + }, + attribute: Self::parse_attr(&attr_str), + } + } + ["vendor", "event", "name"] => DataField::Frame { + attribute: FrameAttribute::VendorEventName, + }, + ["vendor", "tracking", "name"] => DataField::Frame { + attribute: FrameAttribute::VendorTrackingName, + }, + ["match", "id"] => DataField::Frame { + attribute: FrameAttribute::MatchId, + }, + [attr] => DataField::Frame { + attribute: FrameAttribute::from_str(attr) + .unwrap_or_else(|_| FrameAttribute::Other(attr.to_string())), + }, + _ => DataField::Frame { + attribute: FrameAttribute::Other(path.to_string()), + }, + } + } + + /// Helper to safely parse attributes with a fallback to `Other`. + fn parse_attr(s: &str) -> TrackedEntityAttribute { + TrackedEntityAttribute::from_str(s) + .unwrap_or_else(|_| TrackedEntityAttribute::Other(String::from(s))) + } + + /// Returns the recommended Apache Arrow data type for this field. + pub fn default_arrow_type(&self) -> DataType { + match self { + DataField::Frame { attribute } => attribute.default_arrow_type(), + DataField::TrackedEntity { attribute, .. } => attribute.default_arrow_type(), + } + } + + /// Returns true if this field typically has low cardinality. + pub fn is_low_cardinality(&self) -> bool { + match self { + DataField::Frame { attribute } => attribute.is_low_cardinality(), + DataField::TrackedEntity { attribute, .. } => attribute.is_low_cardinality(), + } + } +} + +impl fmt::Display for DataField { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name()) + } +} diff --git a/crates/tdc-core/src/schema/fields.rs b/crates/tdc-core/src/schema/fields.rs new file mode 100644 index 0000000..a883f94 --- /dev/null +++ b/crates/tdc-core/src/schema/fields.rs @@ -0,0 +1,355 @@ +//! Logic for managing and validating a collection of data fields. + +use super::{DataField, FrameAttribute, TrackedEntity, TrackedEntityAttribute}; +use crate::error::{ + TrackingDataContainerError, TrackingDataContainerErrorKind, TrackingDataContainerResult, +}; + +/// Represents the ordered collection of fields (columns) in a tracking dataset. +#[derive(Clone, Debug, PartialEq)] +pub struct Fields { + /// The underlying list of domain-specific data fields. + pub fields: Vec, +} + +impl Fields { + /// Creates a new `Fields` collection from a list of hierarchical column names. + pub fn new(columns: Vec) -> Self { + let fields = columns.iter().map(|c| DataField::from_name(c)).collect(); + Self { fields } + } + + /// Returns the index of the specified field, if it exists. + pub fn get_index(&self, field: &DataField) -> Option { + self.fields.iter().position(|f| f == field) + } + + /// Returns the indices of the specified fields in the current schema. + pub fn get_column_indices(&self, fields: &[DataField]) -> Vec> { + let field_map: std::collections::HashMap = self + .fields + .iter() + .enumerate() + .map(|(i, f)| (f.name(), i)) + .collect(); + + fields + .iter() + .map(|f| field_map.get(&f.name()).copied()) + .collect() + } + + /// Returns the field at the specified index, if it exists. + pub fn get(&self, index: usize) -> Option<&DataField> { + self.fields.get(index) + } + + /// Returns the number of fields in the collection. + pub fn len(&self) -> usize { + self.fields.len() + } + + /// Returns `true` if the collection contains no fields. + pub fn is_empty(&self) -> bool { + self.fields.is_empty() + } + + /// Returns a list of unique player IDs present in the fields, optionally filtered by team. + pub fn players(&self, team_id: Option<&str>) -> Vec { + let mut players = std::collections::HashSet::new(); + for field in &self.fields { + if let DataField::TrackedEntity { + entity: + TrackedEntity::Player { + team_id: tid, + player_id, + }, + .. + } = field + && team_id.is_none_or(|t| t == tid) + { + players.insert(player_id.clone()); + } + } + let mut result: Vec = players.into_iter().collect(); + result.sort(); + result + } + + /// Returns a list of unique player entities present in the fields, optionally filtered by team. + pub fn player_entities(&self, team_id: Option<&str>) -> Vec { + let mut players = std::collections::HashSet::new(); + for field in &self.fields { + if let DataField::TrackedEntity { + entity: entity @ TrackedEntity::Player { team_id: tid, .. }, + .. + } = field + && team_id.is_none_or(|t| t == tid) + { + players.insert(entity.clone()); + } + } + let mut result: Vec = players.into_iter().collect(); + result.sort_by_key(|e| match e { + TrackedEntity::Player { team_id, player_id } => (team_id.clone(), player_id.clone()), + _ => unreachable!(), + }); + result + } + + /// Returns a list of unique team IDs present in the fields. + pub fn teams(&self) -> Vec { + let mut teams = std::collections::HashSet::new(); + for field in &self.fields { + match field { + DataField::TrackedEntity { + entity: TrackedEntity::Team { team_id }, + .. + } => { + teams.insert(team_id.clone()); + } + DataField::TrackedEntity { + entity: TrackedEntity::Player { team_id, .. }, + .. + } => { + teams.insert(team_id.clone()); + } + _ => {} + } + } + let mut result: Vec = teams.into_iter().collect(); + result.sort(); + result + } + + /// Returns a list of unique team entities present in the fields. + pub fn team_entities(&self) -> Vec { + let mut teams = std::collections::HashSet::new(); + for field in &self.fields { + match field { + DataField::TrackedEntity { + entity: entity @ TrackedEntity::Team { .. }, + .. + } => { + teams.insert(entity.clone()); + } + DataField::TrackedEntity { + entity: TrackedEntity::Player { team_id, .. }, + .. + } => { + teams.insert(TrackedEntity::Team { + team_id: team_id.clone(), + }); + } + _ => {} + } + } + let mut result: Vec = teams.into_iter().collect(); + result.sort_by_key(|e| match e { + TrackedEntity::Team { team_id } => team_id.clone(), + _ => unreachable!(), + }); + result + } + + /// Returns a list of unique referee IDs present in the fields. + pub fn referees(&self) -> Vec { + let mut referees = std::collections::HashSet::new(); + for field in &self.fields { + if let DataField::TrackedEntity { + entity: TrackedEntity::Referee { referee_id }, + .. + } = field + { + referees.insert(referee_id.clone()); + } + } + let mut result: Vec = referees.into_iter().collect(); + result.sort(); + result + } + + /// Returns a list of unique referee entities present in the fields. + pub fn referee_entities(&self) -> Vec { + let mut referees = std::collections::HashSet::new(); + for field in &self.fields { + if let DataField::TrackedEntity { + entity: entity @ TrackedEntity::Referee { .. }, + .. + } = field + { + referees.insert(entity.clone()); + } + } + let mut result: Vec = referees.into_iter().collect(); + result.sort_by_key(|e| match e { + TrackedEntity::Referee { referee_id } => referee_id.clone(), + _ => unreachable!(), + }); + result + } + + /// Validates the fields against the **Common Data Format Tracking Data Schema (Version 0.2.2)**. + /// + /// This method checks for the presence of mandatory fields such as `frame_id`, `ball/x/y/z`, + /// and team identifiers. It also ensures that both the home and away teams have at least + /// one player with required coordinates. + /// + /// # Errors + /// + /// Returns a [`TrackingDataContainerError`] of kind `SchemaError` if required fields are missing. + pub fn validate(&self) -> TrackingDataContainerResult<()> { + let mut missing = Vec::new(); + + // 1. Required top-level fields + if !self.has_frame_attr(FrameAttribute::FrameId) { + missing.push("frame_id"); + } + if !self.has_frame_attr(FrameAttribute::Timestamp) { + missing.push("timestamp"); + } + if !self.has_frame_attr(FrameAttribute::Period) { + missing.push("period"); + } + if !self.has_frame_attr(FrameAttribute::MatchId) { + missing.push("match/id"); + } + + // 2. Required ball fields + if !self.has_ball_attr(TrackedEntityAttribute::X) { + missing.push("ball/x"); + } + if !self.has_ball_attr(TrackedEntityAttribute::Y) { + missing.push("ball/y"); + } + if !self.has_ball_attr(TrackedEntityAttribute::Z) { + missing.push("ball/z"); + } + + // 3. Required team fields + let home_exists = self.has_team("home"); + let away_exists = self.has_team("away"); + + if !home_exists { + missing.push("teams/home"); + } else { + if !self.has_team_attr("home", TrackedEntityAttribute::Id) { + missing.push("teams/home/id"); + } + if !self.has_any_player("home") { + missing.push("teams/home/players"); + } + } + + if !away_exists { + missing.push("teams/away"); + } else { + if !self.has_team_attr("away", TrackedEntityAttribute::Id) { + missing.push("teams/away/id"); + } + if !self.has_any_player("away") { + missing.push("teams/away/players"); + } + } + + if !missing.is_empty() { + return Err(TrackingDataContainerError::new( + TrackingDataContainerErrorKind::SchemaError(format!( + "Missing required fields: {}", + missing.join(", ") + )), + )); + } + + // 4. Check for unsupported fields + let mut unsupported = Vec::new(); + for field in &self.fields { + match field { + DataField::Frame { + attribute: FrameAttribute::Other(name), + } => { + unsupported.push(name.clone()); + } + DataField::TrackedEntity { + attribute: TrackedEntityAttribute::Other(_), + .. + } => { + unsupported.push(field.name()); + } + _ => {} + } + } + + if !unsupported.is_empty() { + return Err(TrackingDataContainerError::new( + TrackingDataContainerErrorKind::SchemaWarning(format!( + "Found unsupported fields: {}", + unsupported.join(", ") + )), + )); + } + + Ok(()) + } + + /// Internal helper to check for a specific global frame attribute. + fn has_frame_attr(&self, attr: FrameAttribute) -> bool { + self.fields + .iter() + .any(|f| matches!(f, DataField::Frame { attribute } if *attribute == attr)) + } + + /// Internal helper to check for a specific ball attribute. + fn has_ball_attr(&self, attr: TrackedEntityAttribute) -> bool { + self.fields.iter().any(|f| matches!(f, DataField::TrackedEntity { entity: TrackedEntity::Ball, attribute } if *attribute == attr)) + } + + /// Internal helper to check if any field belongs to a specific team (either team-level or player-level). + fn has_team(&self, tid: &str) -> bool { + self.fields.iter().any(|f| match f { + DataField::TrackedEntity { + entity: TrackedEntity::Team { team_id }, + .. + } => team_id == tid, + DataField::TrackedEntity { + entity: TrackedEntity::Player { team_id, .. }, + .. + } => team_id == tid, + _ => false, + }) + } + + /// Internal helper to check for a specific team-level attribute. + fn has_team_attr(&self, tid: &str, attr: TrackedEntityAttribute) -> bool { + self.fields.iter().any(|f| matches!(f, DataField::TrackedEntity { entity: TrackedEntity::Team { team_id }, attribute } if team_id == tid && *attribute == attr)) + } + + /// Internal helper to verify that at least one player for a team has required spatial data. + fn has_any_player(&self, tid: &str) -> bool { + let mut players_with_x = std::collections::HashSet::new(); + let mut players_with_y = std::collections::HashSet::new(); + + for f in &self.fields { + if let DataField::TrackedEntity { + entity: TrackedEntity::Player { team_id, player_id }, + attribute, + } = f + && team_id == tid + { + if *attribute == TrackedEntityAttribute::X { + players_with_x.insert(player_id); + } + if *attribute == TrackedEntityAttribute::Y { + players_with_y.insert(player_id); + } + } + } + + !players_with_x.is_empty() + && !players_with_y.is_empty() + && players_with_x + .intersection(&players_with_y) + .next() + .is_some() + } +} diff --git a/crates/tdc-core/src/schema/mod.rs b/crates/tdc-core/src/schema/mod.rs new file mode 100644 index 0000000..83a5928 --- /dev/null +++ b/crates/tdc-core/src/schema/mod.rs @@ -0,0 +1,13 @@ +pub mod attribute; +pub mod coordinate_system; +pub mod entity; +pub mod field; +pub mod fields; +pub mod period; + +pub use attribute::{Attribute, FrameAttribute, TrackedEntityAttribute}; +pub use coordinate_system::{CoordinateSystem, Direction, LengthUnit, OrientationMode, Origin}; +pub use entity::TrackedEntity; +pub use field::DataField; +pub use fields::Fields; +pub use period::Period; diff --git a/crates/tdc-core/src/schema/period.rs b/crates/tdc-core/src/schema/period.rs new file mode 100644 index 0000000..7f9b0f1 --- /dev/null +++ b/crates/tdc-core/src/schema/period.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; +use strum_macros::{AsRefStr, Display, EnumString}; + +/// Represents the different periods of a match. +#[derive( + Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, EnumString, AsRefStr, Display, +)] +#[strum(serialize_all = "snake_case")] +pub enum Period { + /// The first half of a regular match. + FirstHalf, + /// The second half of a regular match. + SecondHalf, + /// The first half of extra time. + FirstHalfExtratime, + /// The second half of extra time. + SecondHalfExtratime, + /// Penalty shootout. + Shootout, +} diff --git a/crates/tdc-core/src/selector/expr.rs b/crates/tdc-core/src/selector/expr.rs new file mode 100644 index 0000000..742fe52 --- /dev/null +++ b/crates/tdc-core/src/selector/expr.rs @@ -0,0 +1,128 @@ +use crate::schema::attribute::Attribute; +use crate::schema::{DataField, Fields, TrackedEntity}; + +#[derive(Debug, Clone)] +pub enum Expr { + MatchAll, + Field(DataField), + FieldName(String), + Ball, + Players { + team_id: Option, + }, + Team { + team_id: String, + }, + Attributes { + inner: Box, + names: Vec, + }, + Wildcard(String), +} + +pub fn expand_expr(container: &impl crate::selector::expr::HasSchema, expr: &Expr) -> Vec { + let schema = container.schema(); + match expr { + Expr::MatchAll => schema.fields.iter().map(|f| f.name()).collect(), + Expr::FieldName(name) => vec![name.clone()], + Expr::Field(field) => vec![field.name()], + Expr::Ball => schema + .fields + .iter() + .filter(|f| { + matches!( + f, + DataField::TrackedEntity { + entity: TrackedEntity::Ball, + .. + } + ) + }) + .map(|f| f.name()) + .collect(), + Expr::Players { team_id } => schema + .fields + .iter() + .filter(|f| { + if let DataField::TrackedEntity { + entity: TrackedEntity::Player { team_id: tid, .. }, + .. + } = f + { + team_id.is_none() || team_id.as_ref() == Some(tid) + } else { + false + } + }) + .map(|f| f.name()) + .collect(), + Expr::Team { team_id } => schema + .fields + .iter() + .filter(|f| { + if let DataField::TrackedEntity { + entity: TrackedEntity::Team { team_id: tid }, + .. + } = f + { + team_id == tid + } else { + false + } + }) + .map(|f| f.name()) + .collect(), + Expr::Attributes { inner, names } => { + let base = expand_expr(container, inner); + base.into_iter() + .filter(|name| { + let field = DataField::from_name(name); + match &field { + DataField::Frame { attribute } => { + names.contains(&attribute.name().to_string()) + } + DataField::TrackedEntity { attribute, .. } => { + names.contains(&attribute.name().to_string()) + } + } + }) + .collect() + } + Expr::Wildcard(pattern) => { + let mut result = std::collections::HashSet::new(); + + // Truly robust glob-to-regex + let mut regex_str = pattern.replace(".", r"\."); + + // Replace ** with a special marker first + regex_str = regex_str.replace("/**/", "___DOUBLE_STAR_MID___"); + regex_str = regex_str.replace("**/", "___DOUBLE_STAR_START___"); + regex_str = regex_str.replace("/**", "___DOUBLE_STAR_END___"); + regex_str = regex_str.replace("**", "___DOUBLE_STAR_ANY___"); + + // Replace * + regex_str = regex_str.replace("*", "[^/]*"); + + // Restore markers with regex equivalents + regex_str = regex_str.replace("___DOUBLE_STAR_MID___", "/(?:.*/)?"); + regex_str = regex_str.replace("___DOUBLE_STAR_START___", "(?:.*/)?"); + regex_str = regex_str.replace("___DOUBLE_STAR_END___", "(?:/.*)?"); + regex_str = regex_str.replace("___DOUBLE_STAR_ANY___", ".*"); + + let re = regex::Regex::new(&format!("^{}$", regex_str)).unwrap(); + for field in &schema.fields { + let name = field.name(); + if re.is_match(&name) { + result.insert(name); + } + } + let mut final_list: Vec = result.into_iter().collect(); + final_list.sort(); + final_list + } + } +} + +pub trait HasSchema { + fn schema(&self) -> &Fields; +} diff --git a/crates/tdc-core/src/selector/mod.rs b/crates/tdc-core/src/selector/mod.rs new file mode 100644 index 0000000..3a92afa --- /dev/null +++ b/crates/tdc-core/src/selector/mod.rs @@ -0,0 +1,19 @@ +pub mod expr; + +use crate::error::TrackingDataContainerResult; +use crate::schema::DataField; + +pub trait Select: expr::HasSchema + Sized { + /// Selects a subset of columns from the container. + fn select(&self, fields: Vec) -> TrackingDataContainerResult; + + /// Selects columns based on a selection expression. + fn select_expr(&self, expr: &expr::Expr) -> TrackingDataContainerResult { + let field_names = expr::expand_expr(self, expr); + let fields = field_names + .into_iter() + .map(|name| DataField::from_name(&name)) + .collect(); + self.select(fields) + } +} diff --git a/crates/tdc-core/tests/test_builder.rs b/crates/tdc-core/tests/test_builder.rs new file mode 100644 index 0000000..e50a43c --- /dev/null +++ b/crates/tdc-core/tests/test_builder.rs @@ -0,0 +1,84 @@ +use arrow_array::Array; +use arrow_schema::DataType; +use tdc_core::TrackingDataBuilder; + +#[test] +fn test_builder_basic_ingestion() { + let mut builder = TrackingDataBuilder::new(); + builder.insert_int(1, "period", 1); + builder.insert_float(1, "ball/x", 10.5); + builder.insert_float(2, "ball/x", 20.0); + builder.insert_int(2, "period", 1); + + let container = builder.build().unwrap(); + assert_eq!(container.num_rows(), 2); + + // Check columns + let fields = container.data.fields(); + assert!(fields.iter().any(|f| f.name() == "frame_id")); + assert!(fields.iter().any(|f| f.name() == "period")); + assert!(fields.iter().any(|f| f.name() == "ball/x")); +} + +#[test] +fn test_builder_dictionary_encoding() { + let mut builder = TrackingDataBuilder::new(); + // "period" is marked as low-cardinality in our schema + builder.insert_string(1, "period", "first_half"); + builder.insert_string(2, "period", "first_half"); + builder.insert_string(3, "period", "second_half"); + + let container = builder.build().unwrap(); + let col_idx = container + .data + .fields() + .iter() + .position(|f| f.name() == "period") + .unwrap(); + let array = container.data.column(col_idx); + + assert!(matches!(array.data_type(), DataType::Dictionary(_, _))); +} + +#[test] +fn test_builder_merge() { + let mut builder1 = TrackingDataBuilder::new(); + builder1.insert_float(1, "ball/x", 1.0); + + let mut builder2 = TrackingDataBuilder::new(); + builder2.insert_float(2, "ball/x", 2.0); + + builder1.merge(builder2); + let container = builder1.build().unwrap(); + + assert_eq!(container.num_rows(), 2); +} + +#[test] +fn test_builder_out_of_order() { + let mut builder = TrackingDataBuilder::new(); + builder.insert_float(10, "ball/x", 10.0); + builder.insert_float(1, "ball/x", 1.0); + + let container = builder.build().unwrap(); + assert_eq!(container.num_rows(), 2); +} + +#[test] +fn test_builder_type_conversion() { + let mut builder = TrackingDataBuilder::new(); + let col = "teams/home/players/1/vel"; // Defaults to Float32 + builder.insert_int(1, col, 10); + builder.insert_float(2, col, 20.5); + + let container = builder.build().unwrap(); + let col_idx = container + .data + .fields() + .iter() + .position(|f| f.name() == col) + .unwrap(); + let array = container.data.column(col_idx); + + assert_eq!(array.data_type(), &DataType::Float32); +} diff --git a/crates/tdc-core/tests/test_datacontainer.rs b/crates/tdc-core/tests/test_datacontainer.rs new file mode 100644 index 0000000..ab27749 --- /dev/null +++ b/crates/tdc-core/tests/test_datacontainer.rs @@ -0,0 +1,308 @@ +use arrow_array::{Float32Array, Float64Array}; +use datafusion::prelude::*; +use std::sync::Arc; +use tdc_core::{ + CoordinateSystem, DataField, Direction, LengthUnit, OrientationMode, Origin, Select, + TrackingDataBuilder, +}; + +fn create_test_container() -> tdc_core::TrackingDataContainer { + let mut builder = TrackingDataBuilder::new(); + builder.insert_float(1, "ball/x", 10.0); + builder.insert_float(1, "ball/y", 20.0); + builder.insert_string(1, "period", "first_half"); + builder.insert_string(1, "ball_poss_team_id", "home"); + + builder.insert_float(2, "ball/x", -10.0); + builder.insert_float(2, "ball/y", -20.0); + builder.insert_string(2, "period", "second_half"); + builder.insert_string(2, "ball_poss_team_id", "away"); + + builder.build().unwrap() +} + +#[test] +fn test_container_basics() { + let container = create_test_container(); + assert_eq!(container.num_rows(), 2); + assert!(!container.is_empty()); +} + +#[test] +fn test_container_select() { + let container = create_test_container(); + let selected = container + .select(vec![ + DataField::from_name("ball/x"), + DataField::from_name("period"), + ]) + .unwrap(); + + assert_eq!(selected.num_rows(), 2); + assert_eq!(selected.schema.fields.len(), 2); + assert_eq!(selected.schema.fields[0].name(), "ball/x"); + assert_eq!(selected.schema.fields[1].name(), "period"); +} + +#[test] +fn test_container_with_column() { + let container = create_test_container(); + let new_data: Arc = Arc::new(Float64Array::from(vec![1.0, 2.0])); + let updated = container.with_columns(vec![DataField::from_name("custom/val")], vec![new_data]); + + assert_eq!( + updated.schema.fields.len(), + container.schema.fields.len() + 1 + ); + assert!( + updated + .schema + .fields + .iter() + .any(|f: &DataField| f.name() == "custom/val") + ); +} + +#[test] +fn test_container_with_expr_column() { + let container = create_test_container(); + let ctx = SessionContext::new(); + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + // Add a column that is ball/x * 2 + let updated = container + .with_expr_columns_ctx( + vec!["ball/x_double".to_string()], + vec![col("ball/x").mul(lit(2.0))], + &ctx, + rt.handle(), + ) + .unwrap(); + + let idx = updated + .schema + .fields + .iter() + .position(|f: &DataField| f.name() == "ball/x_double") + .unwrap(); + let array = updated + .data + .column(idx) + .as_any() + .downcast_ref::() + .unwrap(); + + assert_eq!(array.value(0), 20.0); + assert_eq!(array.value(1), -20.0); +} + +#[test] +fn test_container_transform_coordinates() { + let mut container = create_test_container(); + + // Define a source system (Center origin, 105x68 meters) + let source_cs = CoordinateSystem::new( + Some((-52.5, 52.5)), + Some((-34.0, 34.0)), + Origin::Center, + Direction::Right, + Direction::Up, + Some(LengthUnit::Meters), + ); + container = container.with_coordinate_system(source_cs); + + // Define a target system (TopLeft origin, same dimensions) + let target_cs = CoordinateSystem::new( + Some((0.0, 105.0)), + Some((0.0, 68.0)), + Origin::TopLeft, + Direction::Right, + Direction::Down, + Some(LengthUnit::Meters), + ); + + let transformed = container.transform_coordinates(target_cs).unwrap(); + + // ball/x: 10.0 in center-based (-52.5 to 52.5) should be 52.5 + 10.0 = 62.5 in TL-based + let x_idx = transformed + .schema + .fields + .iter() + .position(|f| f.name() == "ball/x") + .unwrap(); + let x_array = transformed + .data + .column(x_idx) + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(x_array.value(0), 62.5f32); + + // ball/y: 20.0 in center-based (Up is positive) should be 34.0 - 20.0 = 14.0 in TL-based (Down is positive) + let y_idx = transformed + .schema + .fields + .iter() + .position(|f| f.name() == "ball/y") + .unwrap(); + let y_array = transformed + .data + .column(y_idx) + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(y_array.value(0), 14.0f32); +} + +#[test] +fn test_container_transform_orientation() { + let container = create_test_container().with_orientation(OrientationMode::StaticHomeAway); + assert_eq!(container.orientation, Some(OrientationMode::StaticHomeAway)); + + // HomeAway: flips second half + let oriented = container + .transform_orientation(OrientationMode::HomeAway) + .unwrap(); + assert_eq!(oriented.orientation, Some(OrientationMode::HomeAway)); + + let x_idx = oriented + .schema + .fields + .iter() + .position(|f| f.name() == "ball/x") + .unwrap(); + let x_array = oriented + .data + .column(x_idx) + .as_any() + .downcast_ref::() + .unwrap(); + + // Frame 1 (first half): x=10.0 -> remains 10.0 + assert_eq!(x_array.value(0), 10.0f32); + // Frame 2 (second half): x=-10.0 -> flipped to 10.0 + assert_eq!(x_array.value(1), 10.0f32); + + // BallOwningTeam: flips when away team has possession + let oriented_poss = container + .transform_orientation(OrientationMode::BallOwningTeam) + .unwrap(); + assert_eq!( + oriented_poss.orientation, + Some(OrientationMode::BallOwningTeam) + ); + let x_array_poss = oriented_poss + .data + .column(x_idx) + .as_any() + .downcast_ref::() + .unwrap(); + + // Frame 1 (home poss): x=10.0 -> remains 10.0 + assert_eq!(x_array_poss.value(0), 10.0f32); + // Frame 2 (away poss): x=-10.0 -> flipped to 10.0 + assert_eq!(x_array_poss.value(1), 10.0f32); +} + +#[test] +fn test_container_transform_orientation_double() { + let container = create_test_container().with_orientation(OrientationMode::StaticHomeAway); + + // First transform: to AwayHome (flips first half) + let oriented1 = container + .transform_orientation(OrientationMode::AwayHome) + .unwrap(); + let x_idx = oriented1 + .schema + .fields + .iter() + .position(|f| f.name() == "ball/x") + .unwrap(); + let x_array1 = oriented1 + .data + .column(x_idx) + .as_any() + .downcast_ref::() + .unwrap(); + + // Frame 1 (first half): x=10.0 -> flipped to -10.0 + assert_eq!(x_array1.value(0), -10.0f32); + // Frame 2 (second half): x=-10.0 -> remains -10.0 + assert_eq!(x_array1.value(1), -10.0f32); + + // Second transform: from AwayHome to HomeAway (flips second half, un-flips first half) + let oriented2 = oriented1 + .transform_orientation(OrientationMode::HomeAway) + .unwrap(); + let x_array2 = oriented2 + .data + .column(x_idx) + .as_any() + .downcast_ref::() + .unwrap(); + + // Frame 1 (first half): was -10.0 -> should be 10.0 again + assert_eq!(x_array2.value(0), 10.0f32); + // Frame 2 (second half): was -10.0 -> should be 10.0 (flipped from original -10.0) + assert_eq!(x_array2.value(1), 10.0f32); +} + +#[test] +fn test_container_transform_orientation_fails_without_orientation() { + let mut builder = TrackingDataBuilder::new(); + builder.insert_float(1, "ball/x", 10.0); + builder.insert_string(1, "period", "first_half"); + let container = builder.build().unwrap(); + assert_eq!(container.orientation, None); + + let result = container.transform_orientation(OrientationMode::AwayHome); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "invalid parameters: No orientation" + ); +} + +#[test] +fn test_container_with_orientation() { + let container = create_test_container(); + let updated = container.with_orientation(OrientationMode::StaticHomeAway); + assert_eq!(updated.orientation, Some(OrientationMode::StaticHomeAway)); +} + +#[test] +fn test_container_transform_coordinates_fails_without_system() { + let container = create_test_container(); + let target_cs = CoordinateSystem::new( + None, + None, + Origin::Center, + Direction::Right, + Direction::Up, + None, + ); + let result = container.transform_coordinates(target_cs); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "invalid parameters: No coordinate system" + ); +} + +#[test] +fn test_container_transform_orientation_fails_without_period() { + let container = create_test_container().with_orientation(OrientationMode::StaticHomeAway); + // Select only ball/x to remove the period column + let container_no_period = container + .select(vec![DataField::from_name("ball/x")]) + .unwrap(); + + let result = container_no_period.transform_orientation(OrientationMode::HomeAway); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "schema error: Missing period column" + ); +} diff --git a/crates/tdc-core/tests/test_fields.rs b/crates/tdc-core/tests/test_fields.rs new file mode 100644 index 0000000..6d783ac --- /dev/null +++ b/crates/tdc-core/tests/test_fields.rs @@ -0,0 +1,54 @@ +use tdc_core::schema::{Fields, TrackedEntity}; + +#[test] +fn test_fields_entities() { + let cols = vec![ + "frame_id".to_string(), + "teams/home/players/1/x".to_string(), + "teams/home/players/2/x".to_string(), + "teams/away/players/10/x".to_string(), + "teams/home/id".to_string(), + "teams/away/formation".to_string(), + "referees/ref_1/x".to_string(), + "ball/x".to_string(), + ]; + let fields = Fields::new(cols); + + // Test players + let players = fields.players(None); + assert_eq!(players.len(), 3); + assert!(players.contains(&"1".to_string())); + assert!(players.contains(&"2".to_string())); + assert!(players.contains(&"10".to_string())); + + let home_players = fields.players(Some("home")); + assert_eq!(home_players.len(), 2); + assert!(home_players.contains(&"1".to_string())); + assert!(home_players.contains(&"2".to_string())); + + // Test teams + let teams = fields.teams(); + assert_eq!(teams.len(), 2); + assert!(teams.contains(&"home".to_string())); + assert!(teams.contains(&"away".to_string())); + + let team_entities = fields.team_entities(); + assert_eq!(team_entities.len(), 2); + assert!(team_entities.contains(&TrackedEntity::Team { + team_id: "home".to_string() + })); + assert!(team_entities.contains(&TrackedEntity::Team { + team_id: "away".to_string() + })); + + // Test referees + let referees = fields.referees(); + assert_eq!(referees.len(), 1); + assert!(referees.contains(&"ref_1".to_string())); + + let referee_entities = fields.referee_entities(); + assert_eq!(referee_entities.len(), 1); + assert!(referee_entities.contains(&TrackedEntity::Referee { + referee_id: "ref_1".to_string() + })); +} diff --git a/crates/tdc-core/tests/test_schema.rs b/crates/tdc-core/tests/test_schema.rs new file mode 100644 index 0000000..b0a78f8 --- /dev/null +++ b/crates/tdc-core/tests/test_schema.rs @@ -0,0 +1,142 @@ +use arrow_schema::{DataType, TimeUnit}; +use std::str::FromStr; +use tdc_core::{DataField, FrameAttribute, TrackedEntity, TrackedEntityAttribute}; + +#[test] +fn test_data_field_from_name() { + // 1. Frame attributes + assert_eq!( + DataField::from_name("timestamp"), + DataField::Frame { + attribute: FrameAttribute::Timestamp + } + ); + assert_eq!( + DataField::from_name("match/id"), + DataField::Frame { + attribute: FrameAttribute::MatchId + } + ); + assert_eq!( + DataField::from_name("vendor/tracking/name"), + DataField::Frame { + attribute: FrameAttribute::VendorTrackingName + } + ); + + // 2. Tracked Entity: Player + let field = DataField::from_name("teams/home/players/1/x"); + match field { + DataField::TrackedEntity { entity, attribute } => { + assert!( + matches!(entity, TrackedEntity::Player { team_id, player_id } if team_id == "home" && player_id == "1") + ); + assert_eq!(attribute, TrackedEntityAttribute::X); + } + _ => panic!("Expected Player attribute"), + } + + // 3. Tracked Entity: Ball + let field = DataField::from_name("ball/z"); + match field { + DataField::TrackedEntity { entity, attribute } => { + assert!(matches!(entity, TrackedEntity::Ball)); + assert_eq!(attribute, TrackedEntityAttribute::Z); + } + _ => panic!("Expected Ball attribute"), + } + + // 4. Tracked Entity: Referee + let field = DataField::from_name("referees/REF_1/y"); + match field { + DataField::TrackedEntity { entity, attribute } => { + assert!( + matches!(entity, TrackedEntity::Referee { referee_id } if referee_id == "REF_1") + ); + assert_eq!(attribute, TrackedEntityAttribute::Y); + } + _ => panic!("Expected Referee attribute"), + } + + // 5. Tracked Entity: Team + let field = DataField::from_name("teams/away/jersey_colour"); + match field { + DataField::TrackedEntity { entity, attribute } => { + assert!(matches!(entity, TrackedEntity::Team { team_id } if team_id == "away")); + assert_eq!(attribute, TrackedEntityAttribute::JerseyColour); + } + _ => panic!("Expected Team attribute"), + } + + // 6. Custom attributes + assert_eq!( + DataField::from_name("ball/spin"), + DataField::TrackedEntity { + entity: TrackedEntity::Ball, + attribute: TrackedEntityAttribute::Other("spin".to_string()) + } + ); + assert_eq!( + DataField::from_name("global_weather"), + DataField::Frame { + attribute: FrameAttribute::Other("global_weather".to_string()) + } + ); +} + +#[test] +fn test_field_name_serialization() { + let cases = vec![ + "teams/home/players/10/x", + "ball/z", + "referees/ref1/vel", + "vendor/tracking/name", + "match/id", + "timestamp", + ]; + for path in cases { + assert_eq!(DataField::from_name(path).name(), path); + } +} + +#[test] +fn test_field_properties() { + // Arrow types + assert_eq!( + DataField::from_name("timestamp").default_arrow_type(), + DataType::Timestamp(TimeUnit::Microsecond, None) + ); + assert_eq!( + DataField::from_name("ball/x").default_arrow_type(), + DataType::Float32 + ); + assert_eq!( + DataField::from_name("ball_status").default_arrow_type(), + DataType::Boolean + ); + + // Cardinality + assert!(DataField::from_name("period").is_low_cardinality()); + assert!(DataField::from_name("teams/home/jersey_colour").is_low_cardinality()); + assert!(!DataField::from_name("ball/x").is_low_cardinality()); +} + +#[test] +fn test_attribute_enums() { + // FrameAttribute parsing + assert_eq!( + FrameAttribute::from_str("timestamp").unwrap(), + FrameAttribute::Timestamp + ); + assert_eq!( + FrameAttribute::from_str("match").unwrap(), + FrameAttribute::MatchId + ); + + // TrackedEntityAttribute display + assert_eq!(TrackedEntityAttribute::X.to_string(), "x"); + assert_eq!( + TrackedEntityAttribute::JerseyColour.to_string(), + "jersey_colour" + ); +} diff --git a/crates/tdc-ops/Cargo.toml b/crates/tdc-ops/Cargo.toml new file mode 100644 index 0000000..8757d45 --- /dev/null +++ b/crates/tdc-ops/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "tdc-ops" +version = "0.1.0-alpha.1" +edition = "2024" +license = "MIT" +description = "Vectorized operations for sports tracking data." + +[lib] +name = "tdc_ops" +crate-type = ["lib"] + +[dependencies] +arrow-array = "54" +arrow-schema = "54" +arrow-cast = "54" +arrow-arith = "54" +arrow-select = "54" +arrow-ord = "54" +pyo3 = { version = "0.23", features = ["abi3-py310"] } +pyo3-arrow = { version = "0.7", default-features = false } diff --git a/crates/tdc-ops/src/lib.rs b/crates/tdc-ops/src/lib.rs new file mode 100644 index 0000000..ec46f54 --- /dev/null +++ b/crates/tdc-ops/src/lib.rs @@ -0,0 +1,277 @@ +use arrow_arith::numeric::{add, mul, sub}; +use arrow_array::{Array, ArrayRef, Float64Array, Scalar}; +use arrow_cast::cast; +use arrow_schema::{ArrowError, DataType}; +use pyo3::IntoPyObjectExt; +use pyo3::prelude::*; +use pyo3::types::PyCapsule; +use std::sync::Arc; + +fn extract_array(obj: &Bound<'_, PyAny>) -> PyResult { + let (array_cap, schema_capsule) = if let Ok(caps) = obj.call_method0("__arrow_c_array__") { + caps.extract::<(Bound<'_, PyCapsule>, Bound<'_, PyCapsule>)>()? + } else { + return Err(pyo3::exceptions::PyTypeError::new_err( + "Expected object with __arrow_c_array__", + )); + }; + + let py_array = pyo3_arrow::PyArray::from_arrow_pycapsule(&array_cap, &schema_capsule)?; + Ok(py_array.into_inner().0) +} + +/// Helper to compute the difference between consecutive elements: values[i] - values[i-1] +/// The first element of the result is 0.0. +fn compute_diff(array: &ArrayRef) -> Result { + let len = array.len(); + if len <= 1 { + return Ok(Arc::new(Float64Array::from(vec![0.0; len])) as ArrayRef); + } + + // array[1..len] + let current = array.slice(1, len - 1); + // array[0..len-1] + let previous = array.slice(0, len - 1); + + let diff = sub(¤t, &previous)?; + + // Prepend 0.0 to match original length + let zero = Float64Array::from(vec![0.0]); + arrow_select::concat::concat(&[&zero, &diff]) +} + +#[pyfunction] +#[pyo3(signature = (x_array, y_array, dt=1.0))] +pub fn compute_speed( + py: Python<'_>, + x_array: Bound<'_, PyAny>, + y_array: Bound<'_, PyAny>, + dt: f64, +) -> PyResult { + let x_arr = extract_array(&x_array)?; + let y_arr = extract_array(&y_array)?; + + let x_f64 = cast(&x_arr, &DataType::Float64) + .map_err(|e: ArrowError| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; + let y_f64 = cast(&y_arr, &DataType::Float64) + .map_err(|e: ArrowError| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; + + let dx = compute_diff(&x_f64) + .map_err(|e: ArrowError| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; + let dy = compute_diff(&y_f64) + .map_err(|e: ArrowError| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; + + // dist2 = dx^2 + dy^2 + let dx2 = mul(&dx, &dx) + .map_err(|e: ArrowError| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; + let dy2 = mul(&dy, &dy) + .map_err(|e: ArrowError| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; + let dist2_arr = add(&dx2, &dy2) + .map_err(|e: ArrowError| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; + + let dist2_f64 = dist2_arr.as_any().downcast_ref::().unwrap(); + let dist_values: Vec = dist2_f64.values().iter().map(|&v| v.sqrt()).collect(); + let dist = Arc::new(Float64Array::from(dist_values)) as ArrayRef; + + // speed = dist / dt + let inv_dt = Scalar::new(Float64Array::from(vec![1.0 / dt])); + let speed = mul(&dist, &inv_dt) + .map_err(|e: ArrowError| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; + + Ok(pyo3_arrow::PyArray::new( + speed, + Arc::new(arrow_schema::Field::new("speed", DataType::Float64, true)), + ) + .into_bound_py_any(py)? + .into()) +} + +#[pyfunction] +#[pyo3(signature = (speed_array, dt=1.0))] +pub fn compute_acceleration( + py: Python<'_>, + speed_array: Bound<'_, PyAny>, + dt: f64, +) -> PyResult { + let arr = extract_array(&speed_array)?; + let f64_arr = cast(&arr, &DataType::Float64) + .map_err(|e: ArrowError| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; + + let dv = compute_diff(&f64_arr) + .map_err(|e: ArrowError| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; + + let inv_dt = Scalar::new(Float64Array::from(vec![1.0 / dt])); + let acc = mul(&dv, &inv_dt) + .map_err(|e: ArrowError| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; + + Ok(pyo3_arrow::PyArray::new( + acc, + Arc::new(arrow_schema::Field::new( + "acceleration", + DataType::Float64, + true, + )), + ) + .into_bound_py_any(py)? + .into()) +} + +#[pyfunction] +#[pyo3(signature = (acc_array, dt=1.0))] +pub fn compute_jerk(py: Python<'_>, acc_array: Bound<'_, PyAny>, dt: f64) -> PyResult { + let arr = extract_array(&acc_array)?; + let f64_arr = cast(&arr, &DataType::Float64) + .map_err(|e: ArrowError| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; + + let da = compute_diff(&f64_arr) + .map_err(|e: ArrowError| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; + + let inv_dt = Scalar::new(Float64Array::from(vec![1.0 / dt])); + let jerk = mul(&da, &inv_dt) + .map_err(|e: ArrowError| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; + + Ok(pyo3_arrow::PyArray::new( + jerk, + Arc::new(arrow_schema::Field::new("jerk", DataType::Float64, true)), + ) + .into_bound_py_any(py)? + .into()) +} + +#[pyfunction] +#[pyo3(signature = (vx_array, vy_array, dt=1.0))] +pub fn compute_angular_velocity( + py: Python<'_>, + vx_array: Bound<'_, PyAny>, + vy_array: Bound<'_, PyAny>, + dt: f64, +) -> PyResult { + let vx_arr = extract_array(&vx_array)?; + let vy_arr = extract_array(&vy_array)?; + + let vx_f64 = cast(&vx_arr, &DataType::Float64) + .map_err(|e: ArrowError| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; + let vx_f64 = vx_f64.as_any().downcast_ref::().unwrap(); + + let vy_f64 = cast(&vy_arr, &DataType::Float64) + .map_err(|e: ArrowError| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; + let vy_f64 = vy_f64.as_any().downcast_ref::().unwrap(); + + let len = vx_f64.len(); + let mut values = vec![0.0; len]; + let (vxs, vys) = (vx_f64.values(), vy_f64.values()); + let inv_dt = 1.0 / dt; + + for i in 1..len { + let theta_prev = vys[i - 1].atan2(vxs[i - 1]); + let theta_curr = vys[i].atan2(vxs[i]); + let mut d_theta = theta_curr - theta_prev; + + // Handle wrap-around + if d_theta > std::f64::consts::PI { + d_theta -= 2.0 * std::f64::consts::PI; + } else if d_theta < -std::f64::consts::PI { + d_theta += 2.0 * std::f64::consts::PI; + } + + values[i] = d_theta * inv_dt; + } + + let result = Arc::new(Float64Array::from(values)) as Arc; + Ok(pyo3_arrow::PyArray::new( + result, + Arc::new(arrow_schema::Field::new( + "angular_velocity", + DataType::Float64, + true, + )), + ) + .into_bound_py_any(py)? + .into()) +} + +#[pyfunction] +#[pyo3(signature = (array, cutoff, fs, order=2))] +pub fn butterworth_filter( + py: Python<'_>, + array: Bound<'_, PyAny>, + cutoff: f64, + fs: f64, + order: usize, +) -> PyResult { + let arr = extract_array(&array)?; + let f64_arr = cast(&arr, &DataType::Float64) + .map_err(|e: ArrowError| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; + let f64_arr = f64_arr.as_any().downcast_ref::().unwrap(); + + let input = f64_arr.values(); + let len = input.len(); + if len == 0 { + return Ok(pyo3_arrow::PyArray::new( + Arc::new(Float64Array::from(Vec::::new())) as Arc, + Arc::new(arrow_schema::Field::new( + "filtered", + DataType::Float64, + true, + )), + ) + .into_bound_py_any(py)? + .into()); + } + + // Only 2nd order implemented for now as it's the most common for tracking data + if order != 2 { + return Err(pyo3::exceptions::PyValueError::new_err( + "Only order=2 is currently supported for butterworth_filter", + )); + } + + // Precompute coefficients for 2nd order Butterworth + // Ref: https://en.wikipedia.org/wiki/Butterworth_filter + let ff = cutoff / fs; + let ita = 1.0 / (std::f64::consts::PI * ff).tan(); + let q = std::f64::consts::SQRT_2; + let b0 = 1.0 / (1.0 + q * ita + ita * ita); + let b1 = 2.0 * b0; + let b2 = b0; + let a1 = 2.0 * (ita * ita - 1.0) * b0; + let a2 = -(1.0 - q * ita + ita * ita) * b0; + + let mut output = vec![0.0; len]; + + // Initial conditions (assume steady state or zero padding) + output[0] = input[0]; + if len > 1 { + output[1] = input[1]; + } + + for i in 2..len { + output[i] = b0 * input[i] + + b1 * input[i - 1] + + b2 * input[i - 2] + + a1 * output[i - 1] + + a2 * output[i - 2]; + } + + let result = Arc::new(Float64Array::from(output)) as Arc; + Ok(pyo3_arrow::PyArray::new( + result, + Arc::new(arrow_schema::Field::new( + "filtered", + DataType::Float64, + true, + )), + ) + .into_bound_py_any(py)? + .into()) +} + +#[pymodule] +fn tdc_ops(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(compute_speed, m)?)?; + m.add_function(wrap_pyfunction!(compute_acceleration, m)?)?; + m.add_function(wrap_pyfunction!(compute_jerk, m)?)?; + m.add_function(wrap_pyfunction!(compute_angular_velocity, m)?)?; + m.add_function(wrap_pyfunction!(butterworth_filter, m)?)?; + Ok(()) +} diff --git a/crates/tdc-parsers/Cargo.toml b/crates/tdc-parsers/Cargo.toml new file mode 100644 index 0000000..d445063 --- /dev/null +++ b/crates/tdc-parsers/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "tdc-parsers" +version = "0.1.0-alpha.1" +edition = "2024" +license = "MIT" +description = "Specialized parsers for sports tracking data formats." + +[lib] +name = "tdc_parsers" +crate-type = ["lib", "cdylib"] + +[dependencies] +tdc-core = { path = "../tdc-core" } +arrow-array = { version = "54" } +arrow-schema = { version = "54" } +quick-xml = "0.37" +rayon = "1.10" +ahash = "0.8" +fast-float = "0.2" +chrono = { version = "^0.4.42", default-features = false } +pyo3 = { version = "0.23", features = ["abi3-py310"], optional = true } + +[features] +default = [] +python = ["dep:pyo3"] + +[dev-dependencies] +tempfile = "3" +arrow-cast = { version = "54" } +criterion = { version = "0.5", features = ["html_reports"] } + +[[bench]] +name = "parsers" +harness = false + +[[bench]] +name = "memory" +harness = false diff --git a/crates/tdc-parsers/benches/memory.rs b/crates/tdc-parsers/benches/memory.rs new file mode 100644 index 0000000..10fc298 --- /dev/null +++ b/crates/tdc-parsers/benches/memory.rs @@ -0,0 +1,40 @@ +use arrow_array::Array; +use criterion::{Criterion, criterion_group, criterion_main}; +use tdc_parsers::{load_sportec, load_statsperform}; + +fn bench_memory(c: &mut Criterion) { + // 1. Sportec + let sportec_path = "../../data/sportec-tracking.xml"; + let sportec_meta = "../../data/sportec-meta.xml"; + let sportec_container = load_sportec(sportec_path, Some(sportec_meta)).unwrap(); + let sportec_size = sportec_container.data.get_array_memory_size(); + + c.bench_function("memory_usage_sportec", |b| { + b.iter(|| { + // We just report the size once, but the bench helps see if there's + // any weird variance in container creation if we wanted to bench that too. + // For now, we use it as a placeholder to report info. + criterion::black_box(sportec_size) + }) + }); + println!( + "Sportec Container Memory Usage: {:.2} MB", + sportec_size as f64 / 1024.0 / 1024.0 + ); + + // 2. StatsPerform + let statsperform_path = "../../data/statsperform-ma25.txt"; + let statsperform_container = load_statsperform(statsperform_path).unwrap(); + let statsperform_size = statsperform_container.data.get_array_memory_size(); + + c.bench_function("memory_usage_statsperform", |b| { + b.iter(|| criterion::black_box(statsperform_size)) + }); + println!( + "StatsPerform Container Memory Usage: {:.2} MB", + statsperform_size as f64 / 1024.0 / 1024.0 + ); +} + +criterion_group!(benches, bench_memory); +criterion_main!(benches); diff --git a/crates/tdc-parsers/benches/parsers.rs b/crates/tdc-parsers/benches/parsers.rs new file mode 100644 index 0000000..ec091fc --- /dev/null +++ b/crates/tdc-parsers/benches/parsers.rs @@ -0,0 +1,20 @@ +use criterion::{Criterion, black_box, criterion_group, criterion_main}; +use tdc_parsers::{load_sportec, load_statsperform}; + +fn bench_parsers(c: &mut Criterion) { + // Sportec Benchmark + let sportec_path = "../../data/sportec-tracking.xml"; + let sportec_meta = "../../data/sportec-meta.xml"; + c.bench_function("parse sportec xml", |b| { + b.iter(|| load_sportec(black_box(sportec_path), Some(black_box(sportec_meta))).unwrap()) + }); + + // StatsPerform Benchmark + let statsperform_path = "../../data/statsperform-ma25.txt"; + c.bench_function("parse statsperform", |b| { + b.iter(|| load_statsperform(black_box(statsperform_path)).unwrap()) + }); +} + +criterion_group!(benches, bench_parsers); +criterion_main!(benches); diff --git a/crates/tdc-parsers/src/lib.rs b/crates/tdc-parsers/src/lib.rs new file mode 100644 index 0000000..a029bd9 --- /dev/null +++ b/crates/tdc-parsers/src/lib.rs @@ -0,0 +1,42 @@ +pub mod sportec; +pub mod statsperform; + +pub use sportec::load_sportec; +pub use statsperform::load_statsperform; + +#[cfg(feature = "python")] +pub mod python { + use pyo3::prelude::*; + use pyo3::types::PyCapsule; + use std::ffi::CString; + + #[pyfunction] + #[pyo3(name = "load_sportec", signature = (path, meta_path=None))] + pub fn load_sportec_py( + py: Python<'_>, + path: String, + meta_path: Option, + ) -> PyResult> { + let container = super::load_sportec(&path, meta_path.as_deref()) + .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; + + let name = CString::new("TrackingDataContainer").unwrap(); + PyCapsule::new(py, container, Some(name)) + } + + #[pyfunction] + #[pyo3(name = "load_statsperform")] + pub fn load_statsperform_py(py: Python<'_>, path: String) -> PyResult> { + let container = super::load_statsperform(&path) + .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; + + let name = CString::new("TrackingDataContainer").unwrap(); + PyCapsule::new(py, container, Some(name)) + } + + pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(load_sportec_py, m)?)?; + m.add_function(wrap_pyfunction!(load_statsperform_py, m)?)?; + Ok(()) + } +} diff --git a/crates/tdc-parsers/src/sportec.rs b/crates/tdc-parsers/src/sportec.rs new file mode 100644 index 0000000..85131b0 --- /dev/null +++ b/crates/tdc-parsers/src/sportec.rs @@ -0,0 +1,474 @@ +use ahash::{AHashMap, AHashSet}; +use arrow_schema::DataType; +use quick_xml::events::Event; +use quick_xml::reader::Reader; +use rayon::prelude::*; +use std::fs; +use tdc_core::datacontainer::{TrackingDataBuilder, TrackingDataContainer}; +use tdc_core::schema::{CoordinateSystem, Direction, LengthUnit, Origin}; + +pub fn load_sportec(path: &str, meta_path: Option<&str>) -> Result { + let raw_data = fs::read_to_string(path).map_err(|e| e.to_string())?; + + // 1. Parse Metadata (Pitch dimensions, Teams) + let mut team_map = AHashMap::new(); + let mut role_to_id = AHashMap::new(); + let mut role_to_name = AHashMap::new(); + let mut role_to_color = AHashMap::new(); + let mut pitch_x = None; + let mut pitch_y = None; + if let Some(mp) = meta_path { + let meta_data = fs::read_to_string(mp).map_err(|e| e.to_string())?; + let mut reader = Reader::from_str(&meta_data); + let mut buf = Vec::new(); + loop { + match reader.read_event_into(&mut buf) { + Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) + if e.name().as_ref() == b"Team" => + { + let mut team_id = String::new(); + let mut role = String::new(); + let mut team_name = String::new(); + let mut team_color = String::new(); + for attr in e.attributes().filter_map(|a| a.ok()) { + match attr.key.as_ref() { + b"TeamId" => { + team_id = String::from_utf8_lossy(&attr.value).into_owned() + } + b"Role" => role = String::from_utf8_lossy(&attr.value).into_owned(), + b"TeamName" => { + team_name = String::from_utf8_lossy(&attr.value).into_owned() + } + b"PlayerShirtMainColor" => { + team_color = String::from_utf8_lossy(&attr.value).into_owned() + } + _ => (), + } + } + if !team_id.is_empty() { + let role_mapped = match role.as_str() { + "home" => "home", + "guest" => "away", + _ => "other", + }; + team_map.insert(team_id.clone(), role_mapped.to_string()); + role_to_id.insert(role_mapped.to_string(), team_id); + if !team_name.is_empty() { + role_to_name.insert(role_mapped.to_string(), team_name); + } + if !team_color.is_empty() { + role_to_color.insert(role_mapped.to_string(), team_color); + } + } + } + Ok(Event::Empty(ref e)) if e.name().as_ref() == b"Environment" => { + for attr in e.attributes().filter_map(|a| a.ok()) { + match attr.key.as_ref() { + b"PitchX" => { + pitch_x = std::str::from_utf8(&attr.value) + .ok() + .and_then(|s| s.parse::().ok()); + } + b"PitchY" => { + pitch_y = std::str::from_utf8(&attr.value) + .ok() + .and_then(|s| s.parse::().ok()); + } + _ => (), + } + } + } + Ok(Event::Eof) => break, + _ => (), + } + buf.clear(); + } + } + + let home_id = role_to_id.get("home").cloned(); + let away_id = role_to_id.get("away").cloned(); + let home_name = role_to_name.get("home").cloned(); + let away_name = role_to_name.get("away").cloned(); + let home_color = role_to_color.get("home").cloned(); + let away_color = role_to_color.get("away").cloned(); + + // 2. Discovery Pass: Find all FrameSets, Frame range, and metadata mappings + let mut fs_positions = Vec::new(); + let mut start = 0; + while let Some(pos) = raw_data[start..].find(" = fs_positions + .par_iter() + .map(|&fs_start| { + let mut entities = AHashSet::new(); + let mut frames = AHashMap::new(); // N -> (Timestamp, Period) + + let mut cur_team = String::new(); + let mut cur_person = String::new(); + + let tag_end = raw_data[fs_start..].find(">").unwrap_or(0); + let mut reader = Reader::from_str(&raw_data[fs_start..fs_start + tag_end + 1]); + let mut buf = Vec::new(); + if let Ok(Event::Start(e)) | Ok(Event::Empty(e)) = reader.read_event_into(&mut buf) { + for a in e.attributes().filter_map(|a| a.ok()) { + match a.key.as_ref() { + b"TeamId" => cur_team = String::from_utf8_lossy(&a.value).into_owned(), + b"PersonId" => cur_person = String::from_utf8_lossy(&a.value).into_owned(), + _ => (), + } + } + } + if !cur_team.is_empty() && !cur_person.is_empty() { + entities.insert((cur_team, cur_person)); + } + + let chunk_end = fs_positions + .iter() + .find(|&&p| p > fs_start) + .cloned() + .unwrap_or(raw_data.len()); + let mut reader = Reader::from_str(&raw_data[fs_start..chunk_end]); + let mut buf = Vec::new(); + loop { + match reader.read_event_into(&mut buf) { + Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) + if e.name().as_ref() == b"Frame" => + { + let mut n = -1; + let mut ts = None; + let mut p = None; + for a in e.attributes().filter_map(|a| a.ok()) { + let val = std::str::from_utf8(&a.value).unwrap_or(""); + match a.key.as_ref() { + b"N" => n = val.parse().unwrap_or(-1), + b"T" => { + if !val.is_empty() { + ts = Some(val.to_string()); + } + } + b"M" => { + if !val.is_empty() { + p = Some(val.parse::().unwrap_or(0)); + } + } + _ => (), + } + } + if n != -1 { + frames.insert(n, (ts, p)); + } + } + Ok(Event::Eof) => break, + _ => (), + } + buf.clear(); + } + (entities, frames) + }) + .collect(); + + let mut global_entities = AHashSet::new(); + let mut global_frames = AHashMap::new(); + for (entities, frames) in discovery_results { + global_entities.extend(entities); + for (n, (ts, p)) in frames { + let entry = global_frames.entry(n).or_insert((None, None)); + if ts.is_some() { + entry.0 = ts; + } + if p.is_some() { + entry.1 = p; + } + } + } + + if global_frames.is_empty() { + return Err("No frames found in Sportec data".to_string()); + } + + let mut sorted_n: Vec = global_frames.keys().cloned().collect(); + sorted_n.sort_unstable(); + + let n_to_row: AHashMap = + sorted_n.iter().enumerate().map(|(i, &n)| (n, i)).collect(); + let num_rows = sorted_n.len(); + + // 3. Data Ingestion Pass (Parallel) + let final_builder = fs_positions + .par_chunks(fs_positions.len().div_ceil(rayon::current_num_threads())) + .map(|pos_group| { + let mut builder = + TrackingDataBuilder::new_fixed(0, (num_rows as i64).saturating_sub(1)); + + // Register columns + builder.get_or_create_column_id( + "timestamp", + DataType::Timestamp(arrow_schema::TimeUnit::Microsecond, None), + ); + builder.get_or_create_column_id("period", DataType::Utf8); + let c_mid = builder.get_or_create_column_id("match/id", DataType::Utf8); + let c_v = builder.get_or_create_column_id("vendor/tracking/name", DataType::Utf8); + let c_bs = builder.get_or_create_column_id("ball_status", DataType::Boolean); + let c_bp = builder.get_or_create_column_id("ball_poss_team_id", DataType::Utf8); + + let c_hi = builder.get_or_create_column_id("teams/home/id", DataType::Utf8); + let c_ai = builder.get_or_create_column_id("teams/away/id", DataType::Utf8); + let c_hn = builder.get_or_create_column_id("teams/home/name", DataType::Utf8); + let c_an = builder.get_or_create_column_id("teams/away/name", DataType::Utf8); + let c_hc = builder.get_or_create_column_id("teams/home/jersey_colour", DataType::Utf8); + let c_ac = builder.get_or_create_column_id("teams/away/jersey_colour", DataType::Utf8); + + let mut entity_col_map = AHashMap::new(); + for (team, person) in &global_entities { + let team_role = team_map.get(team).map(|s| s.as_str()).unwrap_or(team); + let base = match team.as_str() { + "BALL" => "ball".to_string(), + "referee" | "referees" | "Referee" | "Referees" => { + format!("referees/{}", person) + } + _ => format!("teams/{}/players/{}", team_role, person), + }; + let ix = builder.get_or_create_column_id(&format!("{}/x", base), DataType::Float64); + let iy = builder.get_or_create_column_id(&format!("{}/y", base), DataType::Float64); + let iz = if team == "BALL" { + Some(builder.get_or_create_column_id(&format!("{}/z", base), DataType::Float64)) + } else { + None + }; + let iv = + builder.get_or_create_column_id(&format!("{}/vel", base), DataType::Float64); + let ia = + builder.get_or_create_column_id(&format!("{}/acc", base), DataType::Float64); + entity_col_map.insert((team.clone(), person.clone()), (ix, iy, iz, iv, ia)); + } + + let mut buf = Vec::new(); + for &fs_start in pos_group { + let mut cur_team = String::new(); + let mut cur_person = String::new(); + let mut cur_match = String::new(); + + let tag_end = raw_data[fs_start..].find(">").unwrap_or(0); + let mut reader = Reader::from_str(&raw_data[fs_start..fs_start + tag_end + 1]); + if let Ok(Event::Start(e)) | Ok(Event::Empty(e)) = reader.read_event_into(&mut buf) + { + for a in e.attributes().filter_map(|a| a.ok()) { + match a.key.as_ref() { + b"TeamId" => cur_team = String::from_utf8_lossy(&a.value).into_owned(), + b"PersonId" => { + cur_person = String::from_utf8_lossy(&a.value).into_owned() + } + b"MatchId" => { + cur_match = String::from_utf8_lossy(&a.value).into_owned() + } + _ => (), + } + } + } + buf.clear(); + + let chunk_end = fs_positions + .iter() + .find(|&&p| p > fs_start) + .cloned() + .unwrap_or(raw_data.len()); + let mut reader = Reader::from_str(&raw_data[fs_start..chunk_end]); + loop { + match reader.read_event_into(&mut buf) { + Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) + if e.name().as_ref() == b"Frame" => + { + let mut n = -1; + let mut x = 0.0; + let mut y = 0.0; + let mut z = 0.0; + let mut s = 0.0; + let mut a = 0.0; + let mut b_pos = 0; + let mut b_stat = 0; + let mut has_x = false; + let mut has_y = false; + let mut has_z = false; + let mut has_s = false; + let mut has_a = false; + let mut has_bp = false; + let mut has_bs = false; + + for attr in e.attributes().filter_map(|a| a.ok()) { + let val = std::str::from_utf8(&attr.value).unwrap_or(""); + match attr.key.as_ref() { + b"N" => n = val.parse().unwrap_or(-1), + b"X" => { + x = fast_float::parse(val).unwrap_or(0.0); + has_x = true; + } + b"Y" => { + y = fast_float::parse(val).unwrap_or(0.0); + has_y = true; + } + b"Z" => { + z = fast_float::parse(val).unwrap_or(0.0); + has_z = true; + } + b"S" => { + s = fast_float::parse(val).unwrap_or(0.0); + has_s = true; + } + b"A" => { + a = fast_float::parse(val).unwrap_or(0.0); + has_a = true; + } + b"BallPossession" => { + b_pos = val.parse().unwrap_or(0); + has_bp = true; + } + b"BallStatus" => { + b_stat = val.parse().unwrap_or(0); + has_bs = true; + } + _ => (), + } + } + + if let Some(&row) = n_to_row.get(&n) { + if has_bs { + builder.insert_bool_at_idx(row, c_bs, b_stat == 1); + } + if has_bp { + let tid = if b_pos == 1 { + home_id.as_ref() + } else if b_pos == 2 { + away_id.as_ref() + } else { + None + }; + if let Some(t) = tid { + builder.insert_string_at_idx(row, c_bp, t); + } + } + + if !cur_match.is_empty() { + builder.insert_string_at_idx(row, c_mid, &cur_match); + } + builder.insert_string_at_idx(row, c_v, "sportec"); + if let Some(id) = &home_id { + builder.insert_string_at_idx(row, c_hi, id); + } + if let Some(id) = &away_id { + builder.insert_string_at_idx(row, c_ai, id); + } + if let Some(nm) = &home_name { + builder.insert_string_at_idx(row, c_hn, nm); + } + if let Some(nm) = &away_name { + builder.insert_string_at_idx(row, c_an, nm); + } + if let Some(cl) = &home_color { + builder.insert_string_at_idx(row, c_hc, cl); + } + if let Some(cl) = &away_color { + builder.insert_string_at_idx(row, c_ac, cl); + } + + if let Some((ix, iy, iz, iv, ia)) = + entity_col_map.get(&(cur_team.clone(), cur_person.clone())) + { + if has_x { + builder.insert_float_at_idx(row, *ix, x); + } + if has_y { + builder.insert_float_at_idx(row, *iy, y); + } + if let Some(iz_idx) = iz + && has_z + { + builder.insert_float_at_idx(row, *iz_idx, z); + } + if has_s { + builder.insert_float_at_idx(row, *iv, s); + } + if has_a { + builder.insert_float_at_idx(row, *ia, a); + } + } + } + } + Ok(Event::Eof) => break, + _ => (), + } + buf.clear(); + } + } + Ok(builder) + }) + .reduce( + || { + Ok(TrackingDataBuilder::new_fixed( + 0, + (num_rows as i64).saturating_sub(1), + )) + }, + |a: Result, b: Result| match ( + a, b, + ) { + (Ok(mut a), Ok(b)) => { + a.merge(b); + Ok(a) + } + (Err(e), _) | (_, Err(e)) => Err(e), + }, + )?; + + // 4. Finalize Metadata (Interpolate T and M) + let mut final_builder = final_builder; + let c_ts = final_builder.get_or_create_column_id( + "timestamp", + DataType::Timestamp(arrow_schema::TimeUnit::Microsecond, None), + ); + let c_p = final_builder.get_or_create_column_id("period", DataType::Utf8); + let c_fid = final_builder.get_or_create_column_id("frame_id", DataType::Int64); + + let mut last_p = "other".to_string(); + for (i, &n) in sorted_n.iter().enumerate() { + let (ts_str, p_id) = global_frames.get(&n).cloned().unwrap_or((None, None)); + + if let Some(p) = p_id { + last_p = match p { + 1 => "first_half", + 2 => "second_half", + 3 => "first_half_extratime", + 4 => "second_half_extratime", + 5 => "shootout", + _ => "other", + } + .to_string(); + } + final_builder.insert_string_at_idx(i, c_p, &last_p); + final_builder.insert_int_at_idx(i, c_fid, n); + + if let Some(ts) = ts_str + && let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&ts) + { + final_builder.insert_int_at_idx(i, c_ts, dt.timestamp_micros()); + } + } + + let mut container = final_builder.build().map_err(|e| e.to_string())?; + + if let (Some(x), Some(y)) = (pitch_x, pitch_y) { + let cs = CoordinateSystem::new( + Some((-x / 2.0, x / 2.0)), + Some((-y / 2.0, y / 2.0)), + Origin::Center, + Direction::Right, + Direction::Up, + Some(LengthUnit::Meters), + ); + container = container.with_coordinate_system(cs); + } + + Ok(container) +} diff --git a/crates/tdc-parsers/src/statsperform.rs b/crates/tdc-parsers/src/statsperform.rs new file mode 100644 index 0000000..f68c32b --- /dev/null +++ b/crates/tdc-parsers/src/statsperform.rs @@ -0,0 +1,162 @@ +use ahash::AHashMap; +use arrow_schema::DataType; +use rayon::prelude::*; +use std::fs; +use tdc_core::datacontainer::{TrackingDataBuilder, TrackingDataContainer}; +use tdc_core::schema::{CoordinateSystem, Direction, LengthUnit, Origin}; + +pub fn load_statsperform(path: &str) -> Result { + let raw_data = fs::read_to_string(path).map_err(|e| e.to_string())?; + + let lines: Vec<&str> = raw_data.lines().filter(|l| !l.is_empty()).collect(); + if lines.is_empty() { + return Err("Empty file".to_string()); + } + + let num_cpus = rayon::current_num_threads(); + let chunk_size = lines.len().div_ceil(num_cpus); + + let final_builder = lines + .par_chunks(chunk_size) + .map(|chunk| -> Result { + let mut builder = TrackingDataBuilder::new(); + let c_p = builder.get_or_create_column_id("period", DataType::Utf8); + let c_ts = builder.get_or_create_column_id( + "timestamp", + DataType::Timestamp(arrow_schema::TimeUnit::Microsecond, None), + ); + let c_s = builder.get_or_create_column_id("ball_status", DataType::Boolean); + let c_bx = builder.get_or_create_column_id("ball/x", DataType::Float64); + let c_by = builder.get_or_create_column_id("ball/y", DataType::Float64); + let c_bz = builder.get_or_create_column_id("ball/z", DataType::Float64); + let c_v = builder.get_or_create_column_id("vendor/tracking/name", DataType::Utf8); + + let mut player_map = AHashMap::new(); + + for line in chunk { + let mut parts = line.split(':'); + let frame_part = if let Some(p) = parts.next() { + p + } else { + continue; + }; + let (id_s, info_s) = if let Some(s) = frame_part.split_once(';') { + s + } else { + continue; + }; + + let f_id: i64 = id_s.parse().unwrap(); + let row = builder.get_row_index(f_id); + + // Frame info + let mut info = info_s.split(','); + if let (Some(ts_s), Some(p_s), Some(bs_s)) = (info.next(), info.next(), info.next()) + { + let ts_ms: i64 = ts_s.parse().unwrap(); + let p_id: i32 = p_s.parse().unwrap(); + let m_s: i32 = bs_s.parse().unwrap(); + let p_name = match p_id { + 1 => "first_half", + 2 => "second_half", + 3 => "first_half_extratime", + 4 => "second_half_extratime", + 5 => "shootout", + _ => "other", + }; + builder.insert_string_at_idx(row, c_p, p_name); + builder.insert_int_at_idx(row, c_ts, ts_ms * 1000); + builder.insert_bool_at_idx(row, c_s, m_s == 0); + builder.insert_string_at_idx(row, c_v, "statsperform"); + } + + // Players + if let Some(p_part) = parts.next() + && p_part != ";" + { + for p in p_part.split(';') { + if p.is_empty() { + continue; + } + let mut p_i = p.split(','); + let sid_s = p_i.next().unwrap(); + if sid_s == "2" { + continue; + } + let pid = p_i.next().unwrap(); + + let (ix, iy) = *player_map.entry((sid_s, pid)).or_insert_with(|| { + let sid: i32 = sid_s.parse().unwrap(); + let real = if sid > 2 { sid - 3 } else { sid }; + let team = if real == 0 { "home" } else { "away" }; + ( + builder.get_or_create_column_id( + &format!("teams/{}/players/{}/x", team, pid), + DataType::Float64, + ), + builder.get_or_create_column_id( + &format!("teams/{}/players/{}/y", team, pid), + DataType::Float64, + ), + ) + }); + + let _junk = p_i.next(); + if let Some(xs) = p_i.next() + && let Ok(x) = fast_float::parse(xs) + { + builder.insert_float_at_idx(row, ix, x); + } + if let Some(ys) = p_i.next() + && let Ok(y) = fast_float::parse(ys) + { + builder.insert_float_at_idx(row, iy, y); + } + } + } + + // Ball + if let Some(bp) = parts.next() { + let mut b_i = bp.split(';').next().unwrap().split(','); + if let (Some(xs), Some(ys)) = (b_i.next(), b_i.next()) { + if let Ok(x) = fast_float::parse(xs) { + builder.insert_float_at_idx(row, c_bx, x); + } + if let Ok(y) = fast_float::parse(ys) { + builder.insert_float_at_idx(row, c_by, y); + } + if let Some(zs) = b_i.next() + && let Ok(z) = fast_float::parse(zs) + { + builder.insert_float_at_idx(row, c_bz, z); + } + } + } + } + Ok(builder) + }) + .reduce( + || Ok(TrackingDataBuilder::new()), + |a, b| match (a, b) { + (Ok(mut a), Ok(b)) => { + a.merge(b); + Ok(a) + } + (Err(e), _) | (_, Err(e)) => Err(e), + }, + )?; + + let mut container = final_builder.build().map_err(|e| e.to_string())?; + + let cs = CoordinateSystem::new( + None, + None, + Origin::TopLeft, + Direction::Right, + Direction::Down, + Some(LengthUnit::Meters), + ); + container = container.with_coordinate_system(cs); + + Ok(container) +} diff --git a/crates/tdc-parsers/tests/test_sportec.rs b/crates/tdc-parsers/tests/test_sportec.rs new file mode 100644 index 0000000..e024257 --- /dev/null +++ b/crates/tdc-parsers/tests/test_sportec.rs @@ -0,0 +1,202 @@ +use arrow_array::Array; +use std::io::Write; +use tdc_parsers::load_sportec; +use tempfile::NamedTempFile; + +fn create_temp_file(content: &str) -> NamedTempFile { + let mut file = NamedTempFile::new().unwrap(); + write!(file, "{}", content).unwrap(); + file +} + +#[test] +fn test_sportec_parser() { + let xml_content = r#" + + + + + + + + + + + + + +"#; + + let meta_content = r##" + + + + + + + + + +"##; + + let xml_file = create_temp_file(xml_content); + let meta_file = create_temp_file(meta_content); + + let container = load_sportec( + xml_file.path().to_str().unwrap(), + Some(meta_file.path().to_str().unwrap()), + ) + .unwrap(); + + // 1. Verify Schema and Field Mapping + let fields: Vec = container + .schema + .fields + .iter() + .map(|f| f.name().clone()) + .collect(); + + let expected_fields = vec![ + "ball/x", + "ball/y", + "ball/z", + "ball/vel", + "ball/acc", + "referees/DFL-OBJ-00011W/x", + "referees/DFL-OBJ-00011W/y", + "teams/home/players/player1/x", + "teams/home/players/player1/y", + "teams/home/players/player1/vel", + "teams/home/players/player1/acc", + "teams/home/id", + "teams/away/id", + "teams/home/name", + "teams/away/name", + "teams/home/jersey_colour", + "teams/away/jersey_colour", + "timestamp", + "period", + "match/id", + "vendor/tracking/name", + "ball_poss_team_id", + "ball_status", + ]; + + for field in expected_fields { + assert!( + fields.contains(&field.to_string()), + "Missing field: {}", + field + ); + } + + // 2. Verify Data Types and Specific Values + let vendor_idx = container + .schema + .fields + .iter() + .position(|f| f.name() == "vendor/tracking/name") + .unwrap(); + let vendor_col = arrow_cast::cast( + container.data.column(vendor_idx), + &arrow_schema::DataType::Utf8, + ) + .unwrap(); + let vendor_val = vendor_col + .as_any() + .downcast_ref::() + .unwrap() + .value(0); + assert_eq!(vendor_val, "sportec"); + + let status_idx = container + .schema + .fields + .iter() + .position(|f| f.name() == "ball_status") + .unwrap(); + let status_val = container + .data + .column(status_idx) + .as_any() + .downcast_ref::() + .unwrap() + .value(0); + assert!(status_val); + + let ts_idx = container + .schema + .fields + .iter() + .position(|f| f.name() == "timestamp") + .unwrap(); + assert!(matches!( + container.data.column(ts_idx).data_type(), + arrow_schema::DataType::Timestamp(_, _) + )); + + let poss_idx = container + .schema + .fields + .iter() + .position(|f| f.name() == "ball_poss_team_id") + .unwrap(); + let poss_val = container + .data + .column(poss_idx) + .as_any() + .downcast_ref::() + .unwrap() + .value(0); + assert_eq!(poss_val, "TEAM_A"); + + assert_eq!(container.orientation, None); + + // 3. Verify Coordinate System + let cs = container + .coordinate_system + .expect("Coordinate system should be set"); + assert_eq!(cs.x_range, Some((-52.5, 52.5))); + assert_eq!(cs.y_range, Some((-34.0, 34.0))); + assert_eq!(cs.origin, tdc_core::schema::Origin::Center); + assert_eq!(cs.x_direction, tdc_core::schema::Direction::Right); + assert_eq!(cs.y_direction, tdc_core::schema::Direction::Up); + + // 4. Verify Entity Attributes + let check_float = |name: &str, expected: f64| { + let idx = container + .schema + .fields + .iter() + .position(|f| f.name() == name) + .unwrap_or_else(|| panic!("Field {} not found", name)); + let col = container + .data + .column(idx) + .as_any() + .downcast_ref::() + .unwrap_or_else(|| panic!("Field {} is not Float64", name)); + assert!(!col.is_null(0), "Field {} is null", name); + assert!( + (col.value(0) - expected).abs() < 1e-6, + "Field {} value mismatch: expected {}, got {}", + name, + expected, + col.value(0) + ); + }; + + check_float("ball/x", 10.0); + check_float("ball/y", 20.0); + check_float("ball/z", 1.5); + check_float("ball/vel", 5.0); + check_float("ball/acc", 1.0); + + check_float("referees/DFL-OBJ-00011W/x", 30.0); + check_float("referees/DFL-OBJ-00011W/y", 40.0); + + check_float("teams/home/players/player1/x", 50.0); + check_float("teams/home/players/player1/y", 60.0); + check_float("teams/home/players/player1/vel", 8.0); + check_float("teams/home/players/player1/acc", 2.0); +} diff --git a/crates/tdc-parsers/tests/test_statsperform.rs b/crates/tdc-parsers/tests/test_statsperform.rs new file mode 100644 index 0000000..66c13fe --- /dev/null +++ b/crates/tdc-parsers/tests/test_statsperform.rs @@ -0,0 +1,140 @@ +use arrow_array::Array; +use std::io::Write; +use tdc_parsers::load_statsperform; +use tempfile::NamedTempFile; + +fn create_temp_file(content: &str) -> NamedTempFile { + let mut file = NamedTempFile::new().unwrap(); + write!(file, "{}", content).unwrap(); + file +} + +#[test] +fn test_statsperform_parser() { + let content = "1000;12345,1,0:0,player1,10,10.0,20.0;1,player2,11,30.0,40.0:50.0,60.0,1.5"; + let file = create_temp_file(content); + + let container = load_statsperform(file.path().to_str().unwrap()).unwrap(); + + // 1. Verify Schema and Field Mapping + let fields: Vec = container + .schema + .fields + .iter() + .map(|f| f.name().clone()) + .collect(); + + let expected_fields = vec![ + "ball/x", + "ball/y", + "ball/z", + "teams/home/players/player1/x", + "teams/home/players/player1/y", + "teams/away/players/player2/x", + "teams/away/players/player2/y", + "timestamp", + "period", + "vendor/tracking/name", + "ball_status", + ]; + + for field in expected_fields { + assert!( + fields.contains(&field.to_string()), + "Missing field: {}", + field + ); + } + + // 2. Verify Data Types and Specific Values + let vendor_idx = container + .schema + .fields + .iter() + .position(|f| f.name() == "vendor/tracking/name") + .unwrap(); + let vendor_col = arrow_cast::cast( + container.data.column(vendor_idx), + &arrow_schema::DataType::Utf8, + ) + .unwrap(); + let vendor_val = vendor_col + .as_any() + .downcast_ref::() + .unwrap() + .value(0); + assert_eq!(vendor_val, "statsperform"); + + let status_idx = container + .schema + .fields + .iter() + .position(|f| f.name() == "ball_status") + .unwrap(); + let status_val = container + .data + .column(status_idx) + .as_any() + .downcast_ref::() + .unwrap() + .value(0); + assert!(status_val); + + let ts_idx = container + .schema + .fields + .iter() + .position(|f| f.name() == "timestamp") + .unwrap(); + assert!(matches!( + container.data.column(ts_idx).data_type(), + arrow_schema::DataType::Timestamp(_, _) + )); + + assert_eq!(container.orientation, None); + + // 3. Verify Coordinate System + let cs = container + .coordinate_system + .expect("Coordinate system should be set"); + assert_eq!(cs.x_range, None); + assert_eq!(cs.y_range, None); + assert_eq!(cs.origin, tdc_core::schema::Origin::TopLeft); + assert_eq!(cs.x_direction, tdc_core::schema::Direction::Right); + assert_eq!(cs.y_direction, tdc_core::schema::Direction::Down); + assert_eq!(cs.length_unit, Some(tdc_core::schema::LengthUnit::Meters)); + + // 4. Verify Entity Attributes + let check_float = |name: &str, expected: f64| { + let idx = container + .schema + .fields + .iter() + .position(|f| f.name() == name) + .unwrap_or_else(|| panic!("Field {} not found", name)); + let col = container + .data + .column(idx) + .as_any() + .downcast_ref::() + .unwrap_or_else(|| panic!("Field {} is not Float64", name)); + assert!(!col.is_null(0), "Field {} is null", name); + assert!( + (col.value(0) - expected).abs() < 1e-6, + "Field {} value mismatch: expected {}, got {}", + name, + expected, + col.value(0) + ); + }; + + check_float("ball/x", 50.0); + check_float("ball/y", 60.0); + check_float("ball/z", 1.5); + + check_float("teams/home/players/player1/x", 10.0); + check_float("teams/home/players/player1/y", 20.0); + + check_float("teams/away/players/player2/x", 30.0); + check_float("teams/away/players/player2/y", 40.0); +} diff --git a/crates/tdc-python/Cargo.toml b/crates/tdc-python/Cargo.toml new file mode 100644 index 0000000..5b12c86 --- /dev/null +++ b/crates/tdc-python/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "tdc-python" +version = "0.1.0-alpha.1" +edition = "2024" +license = "MIT" +publish = false + +[lib] +name = "_tracking_data_container" +crate-type = ["cdylib"] + +[dependencies] +tdc-core = { path = "../tdc-core" } +tdc-parsers = { path = "../tdc-parsers", features = ["python"] } +tdc-ops = { path = "../tdc-ops" } +pyo3 = { version = "0.23", features = ["abi3-py310", "extension-module"] } +pyo3-arrow = { version = "0.7", default-features = false } +pyo3-log = { version = "0.12" } +datafusion = "45" +datafusion-functions = "45" +datafusion-functions-window = "45" +chrono = { version = "^0.4.42", default-features = false } +tokio = { version = "1", features = ["rt-multi-thread"] } +arrow-array = { version = "54" } +arrow-schema = { version = "54" } +arrow-cast = { version = "54" } +serde = { version = "1.0", features = ["derive"] } +strum = "0.27.2" +once_cell = "1.19" +num_cpus = "1.16" +futures = "0.3" +ahash = "0.8" diff --git a/crates/tdc-python/src/builder.rs b/crates/tdc-python/src/builder.rs new file mode 100644 index 0000000..996d6de --- /dev/null +++ b/crates/tdc-python/src/builder.rs @@ -0,0 +1,84 @@ +use crate::container::PyTrackingDataContainer; +use crate::error::IntoPyResult; +use crate::schema::PyDataField; +use pyo3::prelude::*; +use tdc_core::datacontainer::TrackingDataBuilder; +use tdc_core::error::{TrackingDataContainerError, TrackingDataContainerErrorKind}; + +#[pyclass(name = "_TrackingDataBuilder")] +pub struct PyTrackingDataBuilder { + pub inner: TrackingDataBuilder, +} + +#[pymethods] +impl PyTrackingDataBuilder { + #[new] + fn new() -> Self { + PyTrackingDataBuilder { + inner: TrackingDataBuilder::new(), + } + } + + fn insert_int(&mut self, frame_id: i64, col: Bound<'_, PyAny>, val: i64) -> PyResult<()> { + let col_name = self.extract_col_name(col)?; + self.inner.insert_int(frame_id, &col_name, val); + Ok(()) + } + + fn insert_float(&mut self, frame_id: i64, col: Bound<'_, PyAny>, val: f64) -> PyResult<()> { + let col_name = self.extract_col_name(col)?; + self.inner.insert_float(frame_id, &col_name, val); + Ok(()) + } + + fn insert_string(&mut self, frame_id: i64, col: Bound<'_, PyAny>, val: String) -> PyResult<()> { + let col_name = self.extract_col_name(col)?; + self.inner.insert_string(frame_id, &col_name, &val); + Ok(()) + } + + fn insert_bool(&mut self, frame_id: i64, col: Bound<'_, PyAny>, val: bool) -> PyResult<()> { + let col_name = self.extract_col_name(col)?; + self.inner.insert_bool(frame_id, &col_name, val); + Ok(()) + } + + fn merge(&mut self, other: &mut PyTrackingDataBuilder) { + // We take ownership of other's data by swapping it out + let other_inner = std::mem::replace(&mut other.inner, TrackingDataBuilder::new()); + self.inner.merge(other_inner); + } + + fn build(&mut self) -> PyResult { + // We take the current state to build + let builder = std::mem::replace(&mut self.inner, TrackingDataBuilder::new()); + let container = builder + .build() + .map_err(|e| { + TrackingDataContainerError::new(TrackingDataContainerErrorKind::InvalidParameters( + e.to_string(), + )) + }) + .into_pyresult()?; + Ok(PyTrackingDataContainer { inner: container }) + } +} + +impl PyTrackingDataBuilder { + fn extract_col_name(&self, col: Bound<'_, PyAny>) -> PyResult { + if let Ok(s) = col.extract::() { + Ok(s) + } else if let Ok(f) = col.extract::() { + Ok(f.inner.name()) + } else { + Err(pyo3::exceptions::PyTypeError::new_err( + "Column must be a string or a DataField object", + )) + } + } +} + +pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + Ok(()) +} diff --git a/crates/tdc-python/src/container.rs b/crates/tdc-python/src/container.rs new file mode 100644 index 0000000..c32db11 --- /dev/null +++ b/crates/tdc-python/src/container.rs @@ -0,0 +1,752 @@ +use crate::coordinate_system::{PyCoordinateSystem, PyOrientationMode}; +use crate::error::IntoPyResult; +use crate::schema::{PyDataField, PyFields}; +use crate::selector::{PyColumnSelector, PyExpr, PySelection}; +use tdc_core::Select; +use tdc_core::config::{CONTEXT, RUNTIME}; +use tdc_core::datacontainer::{LazyTrackingDataContainer, TrackingDataContainer}; +use tdc_core::schema::{DataField, Fields}; +use tdc_core::selector::expr::{Expr, expand_expr}; + +use datafusion::arrow::array::builder::{ + BooleanBuilder, Float32Builder, Float64Builder, Int64Builder, StringBuilder, + TimestampMicrosecondBuilder, UInt64Builder, +}; +use datafusion::arrow::array::{Array, ArrayRef, RecordBatch, StructArray}; +use datafusion::arrow::datatypes::{DataType, Field, Fields as ArrowFields, TimeUnit}; +use datafusion::prelude::*; +use futures::StreamExt; +use pyo3::exceptions::{PyTypeError, PyValueError}; +use pyo3::prelude::*; +use pyo3::types::{PyCapsule, PyDict, PyList, PyType}; +use std::sync::Arc; + +fn block_on(future: F) -> F::Output { + RUNTIME.block_on(future) +} + +#[derive(FromPyObject)] +pub enum SelectItem { + Str(String), + Field(PyDataField), + Selection(PySelection), +} +#[derive(FromPyObject)] +pub enum SelectInput { + Single(SelectItem), + List(Vec), +} +#[derive(FromPyObject)] +#[allow(clippy::large_enum_variant)] +pub enum AddColumnValue { + Expr(PyExpr), + Array(PyObject), +} + +#[pyclass(name = "Frame")] +pub struct PyFrame { + pub values: Vec, + pub fields: Vec, + pub field_to_index: std::collections::HashMap, +} + +#[pymethods] +impl PyFrame { + fn __getitem__(&self, py: Python<'_>, key: Bound<'_, PyAny>) -> PyResult { + let name = if let Ok(s) = key.extract::() { + s + } else if let Ok(f) = key.getattr("_field") { + f.extract::()?.inner.name() + } else { + return Err(PyTypeError::new_err("Key must be string or DataField")); + }; + let idx = self + .field_to_index + .get(&name) + .ok_or_else(|| PyValueError::new_err(format!("Field '{}' not found", name)))?; + Ok(self.values[*idx].clone_ref(py)) + } + + fn to_dict(&self, py: Python<'_>) -> PyResult { + let dict = PyDict::new(py); + for (i, field) in self.fields.iter().enumerate() { + dict.set_item(field.name(), self.values[i].clone_ref(py))?; + } + Ok(dict.into_py(py)) + } + + fn __repr__(&self) -> String { + format!("", self.fields.len()) + } +} + +#[pyclass(name = "_TrackingDataContainer")] +pub struct PyTrackingDataContainer { + pub inner: TrackingDataContainer, +} + +#[pymethods] +impl PyTrackingDataContainer { + #[new] + #[pyo3(signature = (data, coordinate_system=None, orientation=None))] + pub fn new( + data: &Bound<'_, PyDict>, + coordinate_system: Option, + orientation: Option, + ) -> PyResult { + let mut fields = Vec::new(); + let mut arrays = Vec::new(); + let mut schema_fields = Vec::new(); + for (key, value) in data { + let col_name = key.extract::()?; + let data_field = DataField::from_name(&col_name); + let target_type = data_field.default_arrow_type(); + let array = build_arrow_array( + value + .downcast::() + .map_err(|_| PyTypeError::new_err("Must be list"))?, + &target_type, + )?; + fields.push(Field::new(&col_name, array.data_type().clone(), true)); + arrays.push(array); + schema_fields.push(data_field); + } + let struct_array = StructArray::try_new(ArrowFields::from(fields), arrays, None) + .map_err(|e| PyValueError::new_err(e.to_string()))?; + let orientation = if let Some(o) = orientation { + use std::str::FromStr; + Some( + tdc_core::schema::OrientationMode::from_str(&o) + .map_err(|e| PyValueError::new_err(e.to_string()))?, + ) + } else { + None + }; + Ok(PyTrackingDataContainer { + inner: TrackingDataContainer { + data: struct_array, + schema: Fields { + fields: schema_fields, + }, + coordinate_system: coordinate_system.map(|cs| cs.inner), + orientation, + }, + }) + } + + fn lazy(&self) -> PyResult { + Ok(PyLazyTrackingDataContainer { + inner: self.inner.lazy().into_pyresult()?, + }) + } + fn select(&self, items: SelectInput) -> PyResult { + let mut fields = Vec::new(); + match items { + SelectInput::Single(item) => fields.extend(self.process_select_item(item)), + SelectInput::List(list) => { + for item in list { + fields.extend(self.process_select_item(item)); + } + } + } + Ok(PyTrackingDataContainer { + inner: self.inner.select(fields).into_pyresult()?, + }) + } + + #[pyo3(signature = (name, value, _schema_capsule=None))] + fn add_column( + &self, + py: Python<'_>, + name: String, + value: AddColumnValue, + _schema_capsule: Option>, + ) -> PyResult { + let schema_capsules = _schema_capsule + .map(|c| PyList::new(py, vec![c])) + .transpose()?; + self.add_columns(py, vec![name], vec![value], schema_capsules) + } + + #[pyo3(signature = (names, values, _schema_capsules=None))] + fn add_columns( + &self, + py: Python<'_>, + names: Vec, + values: Vec, + _schema_capsules: Option>, + ) -> PyResult { + let mut expr_names = Vec::new(); + let mut expr_values = Vec::new(); + let mut array_names = Vec::new(); + let mut array_values = Vec::new(); + for (i, (name, value)) in names.into_iter().zip(values.into_iter()).enumerate() { + match value { + AddColumnValue::Expr(e) => { + expr_names.push(name); + expr_values.push(e.inner); + } + AddColumnValue::Array(obj) => { + array_names.push(DataField::from_name(&name)); + let capsule = _schema_capsules + .as_ref() + .and_then(|l| l.get_item(i).ok()) + .ok_or_else(|| PyValueError::new_err("No schema capsule"))?; + array_values.push( + pyo3_arrow::PyArray::from_arrow_pycapsule( + obj.bind(py).downcast::()?, + capsule.downcast::()?, + )? + .into_inner() + .0, + ); + } + } + } + let mut current = self.inner.clone(); + if !expr_names.is_empty() { + current = current + .with_expr_columns_ctx(expr_names, expr_values, &CONTEXT, RUNTIME.handle()) + .into_pyresult()?; + } + if !array_names.is_empty() { + current = current.with_columns(array_names, array_values); + } + Ok(PyTrackingDataContainer { inner: current }.into_py(py)) + } + + fn add_column_per_player_rust( + &self, + _py: Python<'_>, + _name: String, + _value: AddColumnValue, + ) -> PyResult { + Err(PyValueError::new_err( + "Direct Rust function support has been removed. Use callable passing Arrow capsules instead.", + )) + } + + fn transform_coordinates(&self, cs: PyCoordinateSystem) -> PyResult { + Ok(PyTrackingDataContainer { + inner: self.inner.transform_coordinates(cs.inner).into_pyresult()?, + }) + } + fn transform_orientation(&self, mode: &str) -> PyResult { + use std::str::FromStr; + let mode = tdc_core::schema::OrientationMode::from_str(mode) + .map_err(|e| PyValueError::new_err(e.to_string()))?; + Ok(PyTrackingDataContainer { + inner: self.inner.transform_orientation(mode).into_pyresult()?, + }) + } + fn with_orientation(&self, mode: &str) -> PyResult { + use std::str::FromStr; + let mode = tdc_core::schema::OrientationMode::from_str(mode) + .map_err(|e| PyValueError::new_err(e.to_string()))?; + Ok(PyTrackingDataContainer { + inner: self.inner.clone().with_orientation(mode), + }) + } + fn with_coordinate_system(&self, cs: PyCoordinateSystem) -> Self { + PyTrackingDataContainer { + inner: self.inner.clone().with_coordinate_system(cs.inner), + } + } + + #[classmethod] + fn _from_capsule(_cls: &Bound<'_, PyType>, capsule: &Bound<'_, PyCapsule>) -> PyResult { + let container: &TrackingDataContainer = unsafe { capsule.reference() }; + Ok(PyTrackingDataContainer { + inner: container.clone(), + }) + } + + #[getter] + fn schema(&self) -> PyFields { + PyFields { + inner: self.inner.schema.clone(), + } + } + #[getter] + fn coordinate_system(&self) -> Option { + self.inner + .coordinate_system + .as_ref() + .map(|cs| PyCoordinateSystem { inner: cs.clone() }) + } + #[getter] + fn orientation_mode(&self) -> Option { + self.inner.orientation.map(|o| o.into()) + } + fn num_rows(&self) -> usize { + self.inner.num_rows() + } + fn validate(&self) -> PyResult<()> { + self.inner.validate().into_pyresult() + } + + fn save_parquet(&self, path: &str) -> PyResult<()> { + self.inner.save_parquet(path).into_pyresult() + } + + #[staticmethod] + fn load_parquet(path: &str) -> PyResult { + let inner = TrackingDataContainer::load_parquet(path).into_pyresult()?; + Ok(PyTrackingDataContainer { inner }) + } + + #[getter] + fn selector(&self) -> PyColumnSelector { + PyColumnSelector + } + fn __repr__(&self) -> String { + format!( + "", + self.inner.num_rows(), + self.inner.schema.len() + ) + } + + fn materialize_frame<'py>(&self, py: Python<'py>, frame: usize) -> PyResult { + let mut values = Vec::new(); + let mut field_to_index = std::collections::HashMap::new(); + let mut fields = Vec::new(); + for (i, field) in self.inner.schema.fields.iter().enumerate() { + values.push(arrow_to_python(py, self.inner.data.column(i), frame)?); + field_to_index.insert(field.name(), i); + fields.push(field.clone()); + } + Ok(PyFrame { + values, + fields, + field_to_index, + }) + } + + fn to_arrow<'py>(&self, py: Python<'py>) -> PyResult> { + Ok( + pyo3_arrow::PyRecordBatch::new(RecordBatch::from(self.inner.data.clone())) + .to_pyarrow(py)? + .into_bound(py), + ) + } + fn __arrow_c_schema__<'py>(&self, py: Python<'py>) -> PyResult> { + Ok( + pyo3_arrow::PySchema::new(RecordBatch::from(self.inner.data.clone()).schema()) + .to_pyarrow(py)? + .into_bound(py), + ) + } + fn __arrow_c_array__<'py>( + &self, + py: Python<'py>, + _requested_schema: Option>, + ) -> PyResult> { + let rb = RecordBatch::from(self.inner.data.clone()); + let pyarrow_rb = pyo3_arrow::PyRecordBatch::new(rb).to_pyarrow(py)?; + Ok(pyarrow_rb + .call_method0(py, "__arrow_c_array__")? + .into_bound(py)) + } +} + +impl PyTrackingDataContainer { + fn process_select_item(&self, item: SelectItem) -> Vec { + match item { + SelectItem::Str(s) => { + if s.contains('*') { + let matched_names = expand_expr(&self.inner, &Expr::Wildcard(s)); + self.inner + .schema + .fields + .iter() + .filter(|f| matched_names.contains(&f.name())) + .cloned() + .collect() + } else { + vec![DataField::from_name(&s)] + } + } + SelectItem::Field(f) => vec![f.inner], + SelectItem::Selection(s) => expand_expr(&self.inner, &s.expr) + .into_iter() + .map(|n| DataField::from_name(&n)) + .collect(), + } + } +} + +#[pyclass(name = "_LazyTrackingDataContainer")] +pub struct PyLazyTrackingDataContainer { + pub inner: LazyTrackingDataContainer, +} + +#[pymethods] +impl PyLazyTrackingDataContainer { + fn collect(&self, py: Python<'_>) -> PyResult { + Ok(PyTrackingDataContainer { + inner: py + .allow_threads(|| block_on(async { self.inner.clone().collect().await })) + .into_pyresult()?, + }) + } + fn select(&self, items: SelectInput) -> PyResult { + let mut fields = Vec::new(); + match items { + SelectInput::Single(item) => fields.extend(self.process_select_item(item)), + SelectInput::List(list) => { + for item in list { + fields.extend(self.process_select_item(item)); + } + } + } + Ok(PyLazyTrackingDataContainer { + inner: self.inner.select(fields).into_pyresult()?, + }) + } + fn filter(&self, predicate: PyExpr) -> PyResult { + Ok(PyLazyTrackingDataContainer { + inner: self.inner.filter(predicate.inner).into_pyresult()?, + }) + } + + fn transform_coordinates(&self, cs: PyCoordinateSystem) -> PyResult { + Ok(PyLazyTrackingDataContainer { + inner: self.inner.transform_coordinates(cs.inner).into_pyresult()?, + }) + } + + fn transform_orientation(&self, mode: &str) -> PyResult { + use std::str::FromStr; + let mode = tdc_core::schema::OrientationMode::from_str(mode) + .map_err(|e| PyValueError::new_err(e.to_string()))?; + Ok(PyLazyTrackingDataContainer { + inner: self.inner.transform_orientation(mode).into_pyresult()?, + }) + } + + #[pyo3(signature = (name, value, _schema_capsule=None))] + fn add_column( + &self, + py: Python<'_>, + name: String, + value: AddColumnValue, + _schema_capsule: Option>, + ) -> PyResult { + match value { + AddColumnValue::Expr(e) => Ok(PyLazyTrackingDataContainer { + inner: self.inner.add_column(&name, e.inner).into_pyresult()?, + }), + AddColumnValue::Array(obj) => { + let capsule = + _schema_capsule.ok_or_else(|| PyValueError::new_err("No schema capsule"))?; + let array = pyo3_arrow::PyArray::from_arrow_pycapsule( + obj.bind(py).downcast::()?, + capsule.downcast::()?, + )? + .into_inner() + .0; + Ok(PyLazyTrackingDataContainer { + inner: self.inner.add_array_column(&name, array).into_pyresult()?, + }) + } + } + } + + #[pyo3(signature = (names, values, _schema_capsules=None))] + fn add_columns( + &self, + _py: Python<'_>, + names: Vec, + values: Vec, + _schema_capsules: Option>, + ) -> PyResult { + let mut expr_names = Vec::new(); + let mut expr_values = Vec::new(); + + for (name, value) in names.into_iter().zip(values.into_iter()) { + match value { + AddColumnValue::Expr(e) => { + expr_names.push(name); + expr_values.push(e.inner); + } + AddColumnValue::Array(_) => { + return Err(PyValueError::new_err( + "Adding raw arrays to a lazy container is not yet supported.", + )); + } + } + } + + Ok(PyLazyTrackingDataContainer { + inner: self + .inner + .add_columns(expr_names, expr_values) + .into_pyresult()?, + }) + } + + fn add_column_udf(&self, name: String, func: PyObject, on: Vec) -> PyResult { + let py_udf = crate::udf::PyUDF::new(name.clone(), func); + let scalar_udf = datafusion::logical_expr::ScalarUDF::from(py_udf); + let args: Vec = + on.into_iter().map(datafusion::prelude::col).collect(); + let expr = scalar_udf.call(args); + + Ok(PyLazyTrackingDataContainer { + inner: self.inner.add_column(&name, expr).into_pyresult()?, + }) + } + + #[getter] + fn schema(&self) -> PyFields { + PyFields { + inner: self.inner.schema.clone(), + } + } + #[getter] + fn selector(&self) -> PyColumnSelector { + PyColumnSelector + } + #[getter] + fn orientation_mode(&self) -> Option { + self.inner.orientation.map(|o| o.into()) + } + fn __repr__(&self) -> String { + format!( + "", + self.inner.schema.len() + ) + } + fn __arrow_c_schema__<'py>(&self, py: Python<'py>) -> PyResult> { + Ok( + pyo3_arrow::PySchema::new(Arc::new(self.inner.df.schema().as_arrow().clone())) + .to_pyarrow(py)? + .into_bound(py), + ) + } + fn __arrow_c_stream__<'py>( + &self, + py: Python<'py>, + _requested_schema: Option>, + ) -> PyResult> { + let stream = py + .allow_threads(|| block_on(async { self.inner.df.clone().execute_stream().await })) + .map_err(|e| PyValueError::new_err(e.to_string()))?; + let schema = stream.schema(); + struct StreamReader { + stream: std::pin::Pin>, + schema: arrow_schema::SchemaRef, + } + impl Iterator for StreamReader { + type Item = Result; + fn next(&mut self) -> Option { + let next_batch = block_on(async { self.stream.next().await }); + next_batch.map(|res| { + res.map_err(|e| arrow_schema::ArrowError::ExternalError(Box::new(e))) + }) + } + } + impl datafusion::arrow::record_batch::RecordBatchReader for StreamReader { + fn schema(&self) -> arrow_schema::SchemaRef { + self.schema.clone() + } + } + let pyarrow_reader = + pyo3_arrow::PyRecordBatchReader::new(Box::new(StreamReader { stream, schema })) + .to_pyarrow(py)?; + Ok(pyarrow_reader + .call_method0(py, "__arrow_c_stream__")? + .into_bound(py)) + } +} + +impl PyLazyTrackingDataContainer { + fn process_select_item(&self, item: SelectItem) -> Vec { + match item { + SelectItem::Str(s) => { + if s.contains('*') { + let matched_names = expand_expr(&self.inner, &Expr::Wildcard(s)); + self.inner + .schema + .fields + .iter() + .filter(|f| matched_names.contains(&f.name())) + .cloned() + .collect() + } else { + vec![DataField::from_name(&s)] + } + } + SelectItem::Field(f) => vec![f.inner], + SelectItem::Selection(s) => expand_expr(&self.inner, &s.expr) + .into_iter() + .map(|n| DataField::from_name(&n)) + .collect(), + } + } +} + +#[pyfunction] +pub fn scan_parquet(_py: Python<'_>, path: String) -> PyResult { + let lazy_inner = block_on(async { + let df = CONTEXT + .read_parquet(&path, ParquetReadOptions::default()) + .await?; + let schema = Fields::new( + df.schema() + .as_arrow() + .fields() + .iter() + .map(|f| f.name().clone()) + .collect(), + ); + Ok::( + LazyTrackingDataContainer::new(df, schema, None, None), + ) + }) + .map_err(|e| PyValueError::new_err(e.to_string()))?; + Ok(PyLazyTrackingDataContainer { inner: lazy_inner }) +} + +fn build_arrow_array(list: &Bound<'_, PyList>, dtype: &DataType) -> PyResult { + match dtype { + DataType::Float32 => { + let mut b = Float32Builder::with_capacity(list.len()); + for i in list { + if i.is_none() { + b.append_null(); + } else { + b.append_value(i.extract::()?); + } + } + Ok(Arc::new(b.finish())) + } + DataType::Float64 => { + let mut b = Float64Builder::with_capacity(list.len()); + for i in list { + if i.is_none() { + b.append_null(); + } else { + b.append_value(i.extract::()?); + } + } + Ok(Arc::new(b.finish())) + } + DataType::Int64 => { + let mut b = Int64Builder::with_capacity(list.len()); + for i in list { + if i.is_none() { + b.append_null(); + } else { + b.append_value(i.extract::()?); + } + } + Ok(Arc::new(b.finish())) + } + DataType::UInt64 => { + let mut b = UInt64Builder::with_capacity(list.len()); + for i in list { + if i.is_none() { + b.append_null(); + } else { + b.append_value(i.extract::()?); + } + } + Ok(Arc::new(b.finish())) + } + DataType::Boolean => { + let mut b = BooleanBuilder::with_capacity(list.len()); + for i in list { + if i.is_none() { + b.append_null(); + } else { + b.append_value(i.extract::()?); + } + } + Ok(Arc::new(b.finish())) + } + DataType::Timestamp(TimeUnit::Microsecond, _) => { + let mut b = TimestampMicrosecondBuilder::with_capacity(list.len()); + for i in list { + if i.is_none() { + b.append_null(); + } else { + b.append_value(i.extract::()?); + } + } + Ok(Arc::new(b.finish())) + } + DataType::Utf8 | DataType::Dictionary(_, _) => { + let mut b = StringBuilder::with_capacity(list.len(), list.len() * 5); + for i in list { + if i.is_none() { + b.append_null(); + } else { + b.append_value(i.extract::()?); + } + } + Ok(Arc::new(b.finish())) + } + _ => Err(PyTypeError::new_err(format!( + "Unsupported target type: {:?}", + dtype + ))), + } +} + +fn arrow_to_python(py: Python<'_>, array: &ArrayRef, row: usize) -> PyResult { + use datafusion::arrow::array::AsArray; + if array.is_null(row) { + return Ok(py.None()); + } + match array.data_type() { + DataType::Float32 => Ok(array + .as_primitive::() + .value(row) + .into_py(py)), + DataType::Float64 => Ok(array + .as_primitive::() + .value(row) + .into_py(py)), + DataType::Int64 => Ok(array + .as_primitive::() + .value(row) + .into_py(py)), + DataType::UInt64 => Ok(array + .as_primitive::() + .value(row) + .into_py(py)), + DataType::Boolean => Ok(array.as_boolean().value(row).into_py(py)), + DataType::Utf8 => Ok(array.as_string::().value(row).into_py(py)), + DataType::Timestamp(TimeUnit::Microsecond, _) => Ok(array + .as_primitive::() + .value(row) + .into_py(py)), + DataType::Dictionary(_, _) => { + let dict = + array + .as_any() + .downcast_ref::>() + .unwrap(); + let keys = dict.keys(); + let values = dict + .values() + .as_any() + .downcast_ref::() + .unwrap(); + Ok(values.value(keys.value(row) as usize).into_py(py)) + } + _ => Ok(py.None()), + } +} + +pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(scan_parquet, m)?)?; + Ok(()) +} diff --git a/crates/tdc-python/src/coordinate_system.rs b/crates/tdc-python/src/coordinate_system.rs new file mode 100644 index 0000000..bbcf1c5 --- /dev/null +++ b/crates/tdc-python/src/coordinate_system.rs @@ -0,0 +1,358 @@ +use pyo3::prelude::*; +use std::str::FromStr; +use tdc_core::schema::coordinate_system as core; + +#[pyclass(name = "Origin", eq, eq_int)] +#[derive(Clone, PartialEq)] +/// The location of the origin (0,0) in a coordinate system. +pub enum PyOrigin { + /// The center of the pitch/court. + Center, + /// The top-left corner of the pitch. + TopLeft, + /// The bottom-left corner of the pitch. + BottomLeft, +} + +#[pymethods] +impl PyOrigin { + #[classattr] + const CENTER: Self = Self::Center; + #[classattr] + const TOP_LEFT: Self = Self::TopLeft; + #[classattr] + const BOTTOM_LEFT: Self = Self::BottomLeft; + + fn __str__(&self) -> &str { + match self { + Self::Center => "center", + Self::TopLeft => "top_left", + Self::BottomLeft => "bottom_left", + } + } +} + +impl From for core::Origin { + fn from(o: PyOrigin) -> Self { + match o { + PyOrigin::Center => core::Origin::Center, + PyOrigin::TopLeft => core::Origin::TopLeft, + PyOrigin::BottomLeft => core::Origin::BottomLeft, + } + } +} + +#[pyclass(name = "Direction", eq, eq_int)] +#[derive(Clone, PartialEq)] +/// Possible axis directions. +pub enum PyDirection { + /// Values increase towards the right. + Right, + /// Values increase towards the left. + Left, + /// Values increase towards the top. + Up, + /// Values increase towards the bottom. + Down, +} + +#[pymethods] +impl PyDirection { + #[classattr] + const RIGHT: Self = Self::Right; + #[classattr] + const LEFT: Self = Self::Left; + #[classattr] + const UP: Self = Self::Up; + #[classattr] + const DOWN: Self = Self::Down; + + fn __str__(&self) -> &str { + match self { + Self::Right => "right", + Self::Left => "left", + Self::Up => "up", + Self::Down => "down", + } + } +} + +impl From for core::Direction { + fn from(d: PyDirection) -> Self { + match d { + PyDirection::Right => core::Direction::Right, + PyDirection::Left => core::Direction::Left, + PyDirection::Up => core::Direction::Up, + PyDirection::Down => core::Direction::Down, + } + } +} + +#[pyclass(name = "LengthUnit", eq, eq_int)] +#[derive(Clone, PartialEq)] +/// Standardized units of length. +pub enum PyLengthUnit { + /// Standard metric meter. + Meters, + /// Centimeter. + Centimeters, + /// Millimeter. + Millimeters, + /// Yard (approx 0.9144 meters). + Yards, + /// Inch. + Inches, + /// Foot. + Feet, + /// Normalized unit where coordinates represent a percentage of pitch dimensions. + Percent, +} + +#[pymethods] +impl PyLengthUnit { + #[classattr] + const METERS: Self = Self::Meters; + #[classattr] + const CENTIMETERS: Self = Self::Centimeters; + #[classattr] + const MILLIMETERS: Self = Self::Millimeters; + #[classattr] + const YARDS: Self = Self::Yards; + #[classattr] + const INCHES: Self = Self::Inches; + #[classattr] + const FEET: Self = Self::Feet; + #[classattr] + const PERCENT: Self = Self::Percent; + + fn __str__(&self) -> &str { + match self { + Self::Meters => "meters", + Self::Centimeters => "centimeters", + Self::Millimeters => "millimeters", + Self::Yards => "yards", + Self::Inches => "inches", + Self::Feet => "feet", + Self::Percent => "percent", + } + } +} + +impl From for tdc_core::schema::LengthUnit { + fn from(u: PyLengthUnit) -> Self { + match u { + PyLengthUnit::Meters => tdc_core::schema::LengthUnit::Meters, + PyLengthUnit::Centimeters => tdc_core::schema::LengthUnit::Centimeters, + PyLengthUnit::Millimeters => tdc_core::schema::LengthUnit::Millimeters, + PyLengthUnit::Yards => tdc_core::schema::LengthUnit::Yards, + PyLengthUnit::Inches => tdc_core::schema::LengthUnit::Inches, + PyLengthUnit::Feet => tdc_core::schema::LengthUnit::Feet, + PyLengthUnit::Percent => tdc_core::schema::LengthUnit::Percent, + } + } +} + +#[pyclass(name = "OrientationMode", eq, eq_int)] +#[derive(Clone, PartialEq)] +/// Logic for normalizing playing direction across periods or possession. +pub enum PyOrientationMode { + /// Coordinates are flipped so the team in possession always attacks from left to right. + BallOwningTeam, + /// The home team attacks left-to-right in the first half and right-to-left in the second. + HomeAway, + /// The away team attacks left-to-right in the first half and right-to-left in the second. + AwayHome, + /// The home team always attacks from left to right, regardless of period. + StaticHomeAway, + /// The away team always attacks from left to right, regardless of period. + StaticAwayHome, +} + +#[pymethods] +impl PyOrientationMode { + #[classattr] + const BALL_OWNING_TEAM: Self = Self::BallOwningTeam; + #[classattr] + const HOME_AWAY: Self = Self::HomeAway; + #[classattr] + const AWAY_HOME: Self = Self::AwayHome; + #[classattr] + const STATIC_HOME_AWAY: Self = Self::StaticHomeAway; + #[classattr] + const STATIC_AWAY_HOME: Self = Self::StaticAwayHome; + + fn __str__(&self) -> &str { + match self { + Self::BallOwningTeam => "ball_owning_team", + Self::HomeAway => "home_away", + Self::AwayHome => "away_home", + Self::StaticHomeAway => "static_home_away", + Self::StaticAwayHome => "static_away_home", + } + } +} + +impl From for tdc_core::schema::OrientationMode { + fn from(m: PyOrientationMode) -> Self { + match m { + PyOrientationMode::BallOwningTeam => tdc_core::schema::OrientationMode::BallOwningTeam, + PyOrientationMode::HomeAway => tdc_core::schema::OrientationMode::HomeAway, + PyOrientationMode::AwayHome => tdc_core::schema::OrientationMode::AwayHome, + PyOrientationMode::StaticHomeAway => tdc_core::schema::OrientationMode::StaticHomeAway, + PyOrientationMode::StaticAwayHome => tdc_core::schema::OrientationMode::StaticAwayHome, + } + } +} + +impl From for PyOrientationMode { + fn from(m: tdc_core::schema::OrientationMode) -> Self { + match m { + tdc_core::schema::OrientationMode::BallOwningTeam => PyOrientationMode::BallOwningTeam, + tdc_core::schema::OrientationMode::HomeAway => PyOrientationMode::HomeAway, + tdc_core::schema::OrientationMode::AwayHome => PyOrientationMode::AwayHome, + tdc_core::schema::OrientationMode::StaticHomeAway => PyOrientationMode::StaticHomeAway, + tdc_core::schema::OrientationMode::StaticAwayHome => PyOrientationMode::StaticAwayHome, + } + } +} + +#[pyclass(name = "_CoordinateSystem")] +#[derive(Clone)] +/// Defines a coordinate system for tracking data. +pub struct PyCoordinateSystem { + pub inner: core::CoordinateSystem, +} + +#[derive(FromPyObject)] +pub enum OriginInput { + Enum(PyOrigin), + Str(String), +} + +#[derive(FromPyObject)] +pub enum DirectionInput { + Enum(PyDirection), + Str(String), +} + +#[derive(FromPyObject)] +pub enum LengthUnitInput { + Enum(PyLengthUnit), + Str(String), +} + +#[pymethods] +impl PyCoordinateSystem { + #[new] + #[pyo3(signature = (x_range=None, y_range=None, origin=OriginInput::Str("center".to_string()), x_direction=DirectionInput::Str("right".to_string()), y_direction=DirectionInput::Str("up".to_string()), length_unit=None))] + fn new( + x_range: Option<(f64, f64)>, + y_range: Option<(f64, f64)>, + origin: OriginInput, + x_direction: DirectionInput, + y_direction: DirectionInput, + length_unit: Option, + ) -> PyResult { + let origin = match origin { + OriginInput::Enum(e) => e.into(), + OriginInput::Str(s) => core::Origin::from_str(&s).map_err(|e: strum::ParseError| { + pyo3::exceptions::PyValueError::new_err(e.to_string()) + })?, + }; + let x_direction = match x_direction { + DirectionInput::Enum(e) => e.into(), + DirectionInput::Str(s) => { + core::Direction::from_str(&s).map_err(|e: strum::ParseError| { + pyo3::exceptions::PyValueError::new_err(e.to_string()) + })? + } + }; + let y_direction = match y_direction { + DirectionInput::Enum(e) => e.into(), + DirectionInput::Str(s) => { + core::Direction::from_str(&s).map_err(|e: strum::ParseError| { + pyo3::exceptions::PyValueError::new_err(e.to_string()) + })? + } + }; + let length_unit = if let Some(u) = length_unit { + let core_unit = match u { + LengthUnitInput::Enum(e) => e.into(), + LengthUnitInput::Str(s) => { + tdc_core::schema::LengthUnit::from_str(&s).map_err(|e: strum::ParseError| { + pyo3::exceptions::PyValueError::new_err(e.to_string()) + })? + } + }; + Some(core_unit) + } else { + None + }; + + Ok(PyCoordinateSystem { + inner: core::CoordinateSystem::new( + x_range, + y_range, + origin, + x_direction, + y_direction, + length_unit, + ), + }) + } + + fn __repr__(&self) -> String { + format!( + "", + self.inner.origin, + self.inner.x_direction, + self.inner.y_direction, + self.inner.length_unit + ) + } + + #[getter] + fn origin(&self) -> String { + self.inner.origin.to_string() + } + + #[getter] + fn x_range(&self) -> Option<(f64, f64)> { + self.inner.x_range + } + + #[getter] + fn y_range(&self) -> Option<(f64, f64)> { + self.inner.y_range + } + + #[getter] + fn x_direction(&self) -> String { + self.inner.x_direction.to_string() + } + + #[getter] + fn y_direction(&self) -> String { + self.inner.y_direction.to_string() + } + + #[getter] + fn length_unit(&self) -> Option { + self.inner + .length_unit + .map(|u: tdc_core::schema::LengthUnit| u.to_string()) + } + + fn __eq__(&self, other: &Self) -> bool { + self.inner == other.inner + } +} + +pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/crates/tdc-python/src/error.rs b/crates/tdc-python/src/error.rs new file mode 100644 index 0000000..c3be3d5 --- /dev/null +++ b/crates/tdc-python/src/error.rs @@ -0,0 +1,92 @@ +use pyo3::prelude::*; +use pyo3::{PyErr, PyResult, create_exception, exceptions::PyException}; +use tdc_core::error::{ + TrackingDataContainerError as CoreError, TrackingDataContainerErrorKind, + TrackingDataContainerResult, +}; + +create_exception!( + _tracking_data_container, + TrackingDataContainerError, + PyException, + "The base class for all errors" +); + +create_exception!( + _tracking_data_container, + InvalidParametersError, + TrackingDataContainerError, + "Provided parameters are invalid" +); + +create_exception!( + _tracking_data_container, + SchemaError, + TrackingDataContainerError, + "Schema validation failed" +); + +create_exception!( + _tracking_data_container, + SchemaWarning, + pyo3::exceptions::PyWarning, + "Schema validation warning" +); + +pub fn core_to_py_err(err: CoreError) -> PyErr { + let message = err.to_string(); + match err.kind { + TrackingDataContainerErrorKind::InvalidParameters(_) => { + InvalidParametersError::new_err(message) + } + TrackingDataContainerErrorKind::SchemaError(_) => SchemaError::new_err(message), + TrackingDataContainerErrorKind::SchemaWarning(_) => SchemaWarning::new_err(message), + TrackingDataContainerErrorKind::ArrowError(_) => { + TrackingDataContainerError::new_err(message) + } + TrackingDataContainerErrorKind::DataFusionError(_) => { + TrackingDataContainerError::new_err(message) + } + TrackingDataContainerErrorKind::ParquetError(_) => { + pyo3::exceptions::PyIOError::new_err(message) + } + TrackingDataContainerErrorKind::IOError(_) => pyo3::exceptions::PyIOError::new_err(message), + TrackingDataContainerErrorKind::Internal(_) => { + pyo3::exceptions::PyRuntimeError::new_err(message) + } + } +} + +pub trait IntoPyResult { + type Inner; + fn into_pyresult(self) -> PyResult; +} + +impl IntoPyResult for TrackingDataContainerResult { + type Inner = T; + fn into_pyresult(self) -> PyResult { + self.map_err(core_to_py_err) + } +} + +impl IntoPyResult for Result { + type Inner = T; + fn into_pyresult(self) -> PyResult { + self.map_err(pyo3::exceptions::PyValueError::new_err) + } +} + +pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { + let py = m.py(); + m.add( + "TrackingDataContainerError", + py.get_type::(), + )?; + m.add( + "InvalidParametersError", + py.get_type::(), + )?; + m.add("SchemaError", py.get_type::())?; + m.add("SchemaWarning", py.get_type::())?; + Ok(()) +} diff --git a/crates/tdc-python/src/io.rs b/crates/tdc-python/src/io.rs new file mode 100644 index 0000000..985d51a --- /dev/null +++ b/crates/tdc-python/src/io.rs @@ -0,0 +1,7 @@ +use pyo3::prelude::*; +use tdc_parsers; + +pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { + tdc_parsers::python::register(m)?; + Ok(()) +} diff --git a/crates/tdc-python/src/lib.rs b/crates/tdc-python/src/lib.rs new file mode 100644 index 0000000..2ab7ca8 --- /dev/null +++ b/crates/tdc-python/src/lib.rs @@ -0,0 +1,31 @@ +#![allow(deprecated)] +use pyo3::prelude::*; + +pub mod builder; +pub mod container; +pub mod coordinate_system; +pub mod error; +pub mod io; +pub mod ops; +pub mod schema; +pub mod selector; +pub mod udf; + +#[pymodule(gil_used = false)] +fn _tracking_data_container(m: &Bound<'_, PyModule>) -> PyResult<()> { + pyo3_log::init(); + + builder::register(m)?; + container::register(m)?; + coordinate_system::register(m)?; + error::register(m)?; + schema::register(m)?; + selector::register(m)?; + udf::register(m)?; + io::register(m)?; + ops::register(m)?; + + m.add("__version__", env!("CARGO_PKG_VERSION"))?; + + Ok(()) +} diff --git a/crates/tdc-python/src/mod.rs b/crates/tdc-python/src/mod.rs new file mode 100644 index 0000000..e8ba3d4 --- /dev/null +++ b/crates/tdc-python/src/mod.rs @@ -0,0 +1,20 @@ +pub mod builder; +pub mod container; +pub mod coordinate_system; +pub mod error; +pub mod schema; +pub mod selector; +pub mod udf; + +use pyo3::prelude::*; + +pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { + builder::register(m)?; + container::register(m)?; + coordinate_system::register(m)?; + error::register(m)?; + schema::register(m)?; + selector::register(m)?; + udf::register(m)?; + Ok(()) +} diff --git a/crates/tdc-python/src/ops.rs b/crates/tdc-python/src/ops.rs new file mode 100644 index 0000000..90df574 --- /dev/null +++ b/crates/tdc-python/src/ops.rs @@ -0,0 +1,19 @@ +use pyo3::prelude::*; +use tdc_ops; + +pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { + // We can either register it under a sub-module or directly. + // The user example suggested `import my_rust_lib`, so we'll + // just add the functions to the main module or a sub-module. + + // Let's add it to a sub-module 'ops' + let ops = PyModule::new_bound(m.py(), "ops")?; + ops.add_function(wrap_pyfunction!(tdc_ops::compute_speed, &ops)?)?; + ops.add_function(wrap_pyfunction!(tdc_ops::compute_acceleration, &ops)?)?; + ops.add_function(wrap_pyfunction!(tdc_ops::compute_jerk, &ops)?)?; + ops.add_function(wrap_pyfunction!(tdc_ops::compute_angular_velocity, &ops)?)?; + ops.add_function(wrap_pyfunction!(tdc_ops::butterworth_filter, &ops)?)?; + m.add_submodule(&ops)?; + + Ok(()) +} diff --git a/crates/tdc-python/src/schema.rs b/crates/tdc-python/src/schema.rs new file mode 100644 index 0000000..84fd618 --- /dev/null +++ b/crates/tdc-python/src/schema.rs @@ -0,0 +1,208 @@ +use pyo3::prelude::*; +use pyo3::types::PyType; +use tdc_core::schema::{DataField, Fields, TrackedEntity}; + +#[pyclass(name = "Attr", eq, eq_int)] +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum PyAttr { + X, + Y, + Z, + Speed, + Period, + Timestamp, + FrameId, +} + +#[pyclass(name = "_DataField")] +#[derive(Clone)] +pub struct PyDataField { + pub inner: DataField, +} + +#[pymethods] +impl PyDataField { + #[classmethod] + fn from_name(_cls: &Bound<'_, PyType>, name: &str) -> Self { + PyDataField { + inner: DataField::from_name(name), + } + } + fn is_frame(&self) -> bool { + matches!(self.inner, DataField::Frame { .. }) + } + fn name(&self) -> String { + self.inner.name() + } + fn attribute(&self) -> String { + use tdc_core::schema::attribute::Attribute; + match &self.inner { + DataField::Frame { attribute } => attribute.name().to_string(), + DataField::TrackedEntity { attribute, .. } => attribute.name().to_string(), + } + } + fn entity(&self) -> Option { + match &self.inner { + DataField::Frame { .. } => None, + DataField::TrackedEntity { entity, .. } => Some(PyTrackedEntity { + inner: entity.clone(), + }), + } + } + #[getter] + fn team_id(&self) -> Option { + match &self.inner { + DataField::TrackedEntity { + entity: TrackedEntity::Player { team_id, .. }, + .. + } => Some(team_id.clone()), + _ => None, + } + } + #[getter] + fn player_id(&self) -> Option { + match &self.inner { + DataField::TrackedEntity { + entity: + TrackedEntity::Player { player_id, .. } + | TrackedEntity::Referee { + referee_id: player_id, + }, + .. + } => Some(player_id.clone()), + _ => None, + } + } + fn __repr__(&self) -> String { + self.inner.name() + } +} + +#[pyclass(name = "_TrackedEntity")] +#[derive(Clone)] +pub struct PyTrackedEntity { + pub inner: TrackedEntity, +} + +#[pymethods] +impl PyTrackedEntity { + fn is_ball(&self) -> bool { + matches!(self.inner, TrackedEntity::Ball) + } + fn is_player(&self) -> bool { + matches!(self.inner, TrackedEntity::Player { .. }) + } + fn is_team(&self) -> bool { + matches!(self.inner, TrackedEntity::Team { .. }) + } + fn is_referee(&self) -> bool { + matches!(self.inner, TrackedEntity::Referee { .. }) + } + fn team_id(&self) -> Option { + match &self.inner { + TrackedEntity::Player { team_id, .. } | TrackedEntity::Team { team_id } => { + Some(team_id.clone()) + } + _ => None, + } + } + fn player_id(&self) -> Option { + match &self.inner { + TrackedEntity::Player { player_id, .. } + | TrackedEntity::Referee { + referee_id: player_id, + } => Some(player_id.clone()), + _ => None, + } + } + fn __repr__(&self) -> String { + format!("{:?}", self.inner) + } +} + +#[pyclass(name = "_Fields")] +pub struct PyFields { + pub inner: Fields, +} + +#[pymethods] +impl PyFields { + #[new] + fn new(names: Vec) -> Self { + PyFields { + inner: Fields::new(names), + } + } + fn __len__(&self) -> usize { + self.inner.len() + } + fn __getitem__(&self, index: usize) -> PyResult { + Ok(PyDataField { + inner: self + .inner + .fields + .get(index) + .ok_or_else(|| pyo3::exceptions::PyIndexError::new_err("Index out of range"))? + .clone(), + }) + } + fn player_entities(&self, team_id: Option) -> Vec { + self.inner + .player_entities(team_id.as_deref()) + .into_iter() + .map(|p| PyTrackedEntity { inner: p }) + .collect() + } + fn team_entities(&self) -> Vec { + self.inner + .team_entities() + .into_iter() + .map(|t| PyTrackedEntity { inner: t }) + .collect() + } + fn referee_entities(&self) -> Vec { + self.inner + .referee_entities() + .into_iter() + .map(|r| PyTrackedEntity { inner: r }) + .collect() + } + fn __iter__(slf: PyRef<'_, Self>) -> FieldIterator { + FieldIterator { + fields: slf.inner.fields.clone(), + index: 0, + } + } +} + +#[pyclass] +pub struct FieldIterator { + fields: Vec, + index: usize, +} + +#[pymethods] +impl FieldIterator { + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { + if slf.index < slf.fields.len() { + let res = Some(PyDataField { + inner: slf.fields[slf.index].clone(), + }); + slf.index += 1; + res + } else { + None + } + } +} + +pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/crates/tdc-python/src/selector.rs b/crates/tdc-python/src/selector.rs new file mode 100644 index 0000000..8120786 --- /dev/null +++ b/crates/tdc-python/src/selector.rs @@ -0,0 +1,330 @@ +use datafusion::common::tree_node::TreeNode; +use datafusion::prelude as df; +use datafusion_functions::expr_fn::coalesce; +use datafusion_functions::math::expr_fn::{power, sqrt}; +use datafusion_functions_window::expr_fn::lag; +use pyo3::exceptions::PyTypeError; +use pyo3::prelude::*; +use tdc_core::selector::expr::Expr as CoreExpr; + +#[pyclass(name = "Selection")] +#[derive(Clone)] +pub struct PySelection { + pub expr: CoreExpr, +} + +#[pymethods] +impl PySelection { + fn __call__(&self) -> Self { + self.clone() + } + fn attributes(&self, names: Vec) -> Self { + PySelection { + expr: CoreExpr::Attributes { + inner: Box::new(self.expr.clone()), + names, + }, + } + } + #[getter] + fn expr(&self) -> String { + format!("{:?}", self.expr) + } +} + +#[pyclass(name = "ColumnSelector")] +pub struct PyColumnSelector; + +#[pymethods] +impl PyColumnSelector { + fn ball(&self) -> PySelection { + PySelection { + expr: CoreExpr::Ball, + } + } + fn players(&self, team_id: Option) -> PySelection { + PySelection { + expr: CoreExpr::Players { team_id }, + } + } + fn team(&self, team_id: &str) -> PySelection { + PySelection { + expr: CoreExpr::Players { + team_id: Some(team_id.to_string()), + }, + } + } +} + +#[pyclass(name = "Expr")] +#[derive(Clone)] +pub struct PyExpr { + pub inner: df::Expr, +} + +#[pymethods] +impl PyExpr { + fn __add__(&self, other: PyExpr) -> PyExpr { + PyExpr { + inner: self.inner.clone() + other.inner, + } + } + fn __sub__(&self, other: PyExpr) -> PyExpr { + PyExpr { + inner: self.inner.clone() - other.inner, + } + } + + fn __mul__(&self, other: Bound<'_, PyAny>) -> PyResult { + if let Ok(e) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone() * e.inner, + }) + } else if let Ok(f) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone() * df::lit(f), + }) + } else if let Ok(i) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone() * df::lit(i), + }) + } else { + Err(PyTypeError::new_err("Invalid type for multiplication")) + } + } + + fn mul(&self, other: Bound<'_, PyAny>) -> PyResult { + self.__mul__(other) + } + + fn __truediv__(&self, other: PyExpr) -> PyExpr { + PyExpr { + inner: self.inner.clone() / other.inner, + } + } + fn __pow__(&self, exponent: f64, _modulo: Option>) -> PyExpr { + PyExpr { + inner: power(self.inner.clone(), df::lit(exponent)), + } + } + + fn __gt__(&self, other: Bound<'_, PyAny>) -> PyResult { + if let Ok(e) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone().gt(e.inner), + }) + } else if let Ok(f) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone().gt(df::lit(f)), + }) + } else if let Ok(i) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone().gt(df::lit(i)), + }) + } else { + Err(PyTypeError::new_err("Invalid type for comparison")) + } + } + + fn gt(&self, other: Bound<'_, PyAny>) -> PyResult { + self.__gt__(other) + } + + fn __lt__(&self, other: Bound<'_, PyAny>) -> PyResult { + if let Ok(e) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone().lt(e.inner), + }) + } else if let Ok(f) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone().lt(df::lit(f)), + }) + } else if let Ok(i) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone().lt(df::lit(i)), + }) + } else { + Err(PyTypeError::new_err("Invalid type for comparison")) + } + } + + fn lt(&self, other: Bound<'_, PyAny>) -> PyResult { + self.__lt__(other) + } + + fn __eq__(&self, other: Bound<'_, PyAny>) -> PyResult { + if let Ok(e) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone().eq(e.inner), + }) + } else if let Ok(f) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone().eq(df::lit(f)), + }) + } else if let Ok(i) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone().eq(df::lit(i)), + }) + } else if let Ok(s) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone().eq(df::lit(s)), + }) + } else if let Ok(b) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone().eq(df::lit(b)), + }) + } else { + Err(PyTypeError::new_err("Invalid type for comparison")) + } + } + + fn eq(&self, other: Bound<'_, PyAny>) -> PyResult { + self.__eq__(other) + } + + fn ne(&self, other: Bound<'_, PyAny>) -> PyResult { + if let Ok(e) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone().not_eq(e.inner), + }) + } else if let Ok(f) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone().not_eq(df::lit(f)), + }) + } else if let Ok(i) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone().not_eq(df::lit(i)), + }) + } else if let Ok(s) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone().not_eq(df::lit(s)), + }) + } else if let Ok(b) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone().not_eq(df::lit(b)), + }) + } else { + Err(PyTypeError::new_err("Invalid type for comparison")) + } + } + + fn ge(&self, other: Bound<'_, PyAny>) -> PyResult { + if let Ok(e) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone().gt_eq(e.inner), + }) + } else if let Ok(f) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone().gt_eq(df::lit(f)), + }) + } else if let Ok(i) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone().gt_eq(df::lit(i)), + }) + } else { + Err(PyTypeError::new_err("Invalid type for comparison")) + } + } + + fn le(&self, other: Bound<'_, PyAny>) -> PyResult { + if let Ok(e) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone().lt_eq(e.inner), + }) + } else if let Ok(f) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone().lt_eq(df::lit(f)), + }) + } else if let Ok(i) = other.extract::() { + Ok(PyExpr { + inner: self.inner.clone().lt_eq(df::lit(i)), + }) + } else { + Err(PyTypeError::new_err("Invalid type for comparison")) + } + } + + fn diff(&self) -> Self { + PyExpr { + inner: self.inner.clone() - lag(self.inner.clone(), Some(1), None), + } + } + + fn fill_null(&self, value: Bound<'_, PyAny>) -> PyResult { + let replacement = if let Ok(f) = value.extract::() { + df::lit(f) + } else if let Ok(i) = value.extract::() { + df::lit(i) + } else if let Ok(s) = value.extract::() { + df::lit(s) + } else if let Ok(b) = value.extract::() { + df::lit(b) + } else if let Ok(e) = value.extract::() { + e.inner + } else { + return Err(PyTypeError::new_err("Unsupported type for fill_null")); + }; + + Ok(PyExpr { + inner: coalesce(vec![self.inner.clone(), replacement]), + }) + } + + fn pow(&self, exponent: f64) -> Self { + self.__pow__(exponent, None) + } + fn sqrt(&self) -> Self { + PyExpr { + inner: sqrt(self.inner.clone()), + } + } + + fn replace_columns(&self, mapping: std::collections::HashMap) -> PyExpr { + let transformed = self + .inner + .clone() + .transform(|expr| { + if let df::Expr::Column(ref col) = expr + && let Some(new_name) = mapping.get(&col.name) + { + return Ok(datafusion::common::tree_node::Transformed::yes( + df::Expr::Column(datafusion::common::Column::from_name(new_name)), + )); + } + Ok(datafusion::common::tree_node::Transformed::no(expr)) + }) + .unwrap() + .data; + PyExpr { inner: transformed } + } +} + +#[pyfunction] +pub fn col(name: &str) -> PyExpr { + PyExpr { + inner: df::col(name), + } +} +#[pyfunction] +pub fn lit(value: Bound<'_, PyAny>) -> PyResult { + if let Ok(f) = value.extract::() { + Ok(PyExpr { inner: df::lit(f) }) + } else if let Ok(i) = value.extract::() { + Ok(PyExpr { inner: df::lit(i) }) + } else if let Ok(s) = value.extract::() { + Ok(PyExpr { inner: df::lit(s) }) + } else if let Ok(b) = value.extract::() { + Ok(PyExpr { inner: df::lit(b) }) + } else { + Err(PyTypeError::new_err("Unsupported literal type")) + } +} + +pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(col, m)?)?; + m.add_function(wrap_pyfunction!(lit, m)?)?; + Ok(()) +} diff --git a/crates/tdc-python/src/udf.rs b/crates/tdc-python/src/udf.rs new file mode 100644 index 0000000..c4e34dc --- /dev/null +++ b/crates/tdc-python/src/udf.rs @@ -0,0 +1,169 @@ +use arrow_schema::{DataType, Field}; +use datafusion::error::DataFusionError; +use datafusion::logical_expr::{ColumnarValue, ScalarUDFImpl, Signature, Volatility}; +use pyo3::prelude::*; +use pyo3::types::{PyCapsule, PyTuple}; +use std::sync::Arc; + +#[derive(Debug)] +pub struct PyUDF { + pub name: String, + pub func: PyObject, + pub signature: Signature, +} + +impl PyUDF { + pub fn new(name: String, func: PyObject) -> Self { + Self { + name, + func, + signature: Signature::variadic_any(Volatility::Immutable), + } + } +} + +impl ScalarUDFImpl for PyUDF { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn name(&self) -> &str { + &self.name + } + + fn signature(&self) -> &Signature { + &self.signature + } + + fn return_type(&self, arg_types: &[DataType]) -> Result { + // Use the type of the first argument if available, else default to Float64 + if let Some(t) = arg_types.first() { + Ok(t.clone()) + } else { + Ok(DataType::Float64) + } + } + + fn invoke(&self, args: &[ColumnarValue]) -> Result { + Python::with_gil(|py| { + // 1. Convert ColumnarValues to Python objects (Polars Series) + let mut py_args = Vec::with_capacity(args.len()); + let pl = py.import("polars").map_err(|e| { + DataFusionError::Execution(format!("Failed to import polars: {}", e)) + })?; + let series_class = pl.getattr("Series").map_err(|e| { + DataFusionError::Execution(format!("Failed to get polars.Series: {}", e)) + })?; + + for arg in args { + let array = match arg { + ColumnarValue::Array(array) => array.clone(), + ColumnarValue::Scalar(scalar) => scalar + .to_array() + .map_err(|e| DataFusionError::External(Box::new(e)))?, + }; + + let field = Arc::new(Field::new("arg", array.data_type().clone(), true)); + let py_array = pyo3_arrow::PyArray::new(array, field); + let py_array_bound = Bound::new(py, py_array).map_err(|e| { + DataFusionError::Execution(format!("Failed to create PyArray bound: {}", e)) + })?; + + // Convert pyarrow-compatible object to polars Series + // We use the fact that PyArray implements the Arrow PyCapsule interface + let series = series_class.call1((py_array_bound,)).map_err(|e| { + DataFusionError::Execution(format!("Failed to create polars Series: {}", e)) + })?; + py_args.push(series.unbind()); + } + + // 2. Call the Python function + let args_tuple = PyTuple::new(py, py_args).map_err(|e| { + DataFusionError::Execution(format!("Failed to create args tuple: {}", e)) + })?; + let res = self + .func + .call1(py, args_tuple) + .map_err(|e| DataFusionError::Execution(format!("Python UDF failed: {}", e)))?; + + // 3. Convert result back to ColumnarValue + let res_bound = res.bind(py); + + // Try to extract Arrow capsule + let (array_capsule, schema_capsule) = if let Ok(capsules) = + res_bound.call_method0("__arrow_c_array__") + { + capsules + .extract::<(Bound<'_, PyCapsule>, Bound<'_, PyCapsule>)>() + .map_err(|e| { + DataFusionError::Execution(format!("Failed to extract capsules: {}", e)) + })? + } else if let Ok(capsules) = res_bound + .getattr("__arrow_c_array__") + .and_then(|m| m.call0()) + { + capsules + .extract::<(Bound<'_, PyCapsule>, Bound<'_, PyCapsule>)>() + .map_err(|e| { + DataFusionError::Execution(format!( + "Failed to extract capsules from attribute: {}", + e + )) + })? + } else { + // Fallback: try to convert via pyarrow + let pa = py.import("pyarrow").map_err(|e| { + DataFusionError::Execution(format!("Failed to import pyarrow: {}", e)) + })?; + let array_fn = pa.getattr("array").map_err(|e| { + DataFusionError::Execution(format!("Failed to get pyarrow.array: {}", e)) + })?; + let pa_array = array_fn.call1((res_bound,)).map_err(|e| { + DataFusionError::Execution(format!("Failed to create pyarrow array: {}", e)) + })?; + + let capsules = pa_array.call_method0("__arrow_c_array__").map_err(|e| { + DataFusionError::Execution(format!("Failed to call __arrow_c_array__: {}", e)) + })?; + capsules + .extract::<(Bound<'_, PyCapsule>, Bound<'_, PyCapsule>)>() + .map_err(|e| { + DataFusionError::Execution(format!( + "Failed to extract fallback capsules: {}", + e + )) + })? + }; + + let py_array = + pyo3_arrow::PyArray::from_arrow_pycapsule(&array_capsule, &schema_capsule) + .map_err(|e| { + DataFusionError::Execution(format!( + "Failed to convert result capsule: {}", + e + )) + })?; + let array = py_array.into_inner().0; + + // Ensure result type matches expected return type by casting if necessary + let target_type = + self.return_type(&args.iter().map(|a| a.data_type()).collect::>())?; + let array = if array.data_type() != &target_type { + arrow_cast::cast(&array, &target_type).map_err(|e| { + DataFusionError::Execution(format!( + "Failed to cast UDF result to {}: {}", + target_type, e + )) + })? + } else { + array + }; + + Ok(ColumnarValue::Array(array)) + }) + } +} + +pub fn register(_m: &Bound<'_, PyModule>) -> PyResult<()> { + Ok(()) +} diff --git a/examples/01-basics.ipynb b/examples/01-basics.ipynb new file mode 100644 index 0000000..93f9b5a --- /dev/null +++ b/examples/01-basics.ipynb @@ -0,0 +1,359 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Basics: TrackingDataContainer and CDF Schema\n", + "\n", + "The **TrackingDataContainer** (TDC) is a high-performance, standardized interface for sports tracking data. Backed by **Apache Arrow**, it provides a metadata-aware representation that bridges the gap between raw data parsing packages (e.g., kloppy) and data analytics tools (e.g., floodlight, databallpy)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Why TDC?\n", + "\n", + "Our goal is to define a canonical, metadata-rich container that enables fast, zero-copy data exchange while remaining compatible with the modern data science ecosystem. TDC focuses on the **containerized representation** of tracking data, delegating raw format parsing and analysis to specialized tools.\n", + "\n", + "The main goals are to provide:\n", + "\n", + "* **Scalability**: Overcomes the limitations of object-based models for massive datasets.\n", + "* **Interoperability**: Standardizes column naming and coordinate systems across providers.\n", + "* **Efficiency**: Eliminates duplicated effort in data parsing and management.\n", + "\n", + "These goals could also be achieved by storing data in a standard `pyarrow.Table` or `polars.DataFrame` but these treat tracking data as a collection of generic floats and strings. In contrast, TDC provides **domain-specific logic**. It \"understands\" the data, allowing for abstractions that a generic table cannot provide:\n", + "\n", + "1. **Spatial Metadata**: The container keeps track of the coordinate system and orientation of the data.\n", + "2. **Consistency**: The container enforces a standard schema for typical data fields (e.g., player coordinates).\n", + "3. **Support domain-specific operations**: Standard operations such as normalizing player orientations (so that a team always attacks in the same direction) can be implemented as a single method call." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Why Rust and Apache Arrow?\n", + "\n", + "The core logic of TDC is written in **Rust** and backed by **Apache Arrow**. Under the hood, a `TrackingDataContainer` is a thin wrapper around an Arrow `StructArray`.\n", + "\n", + "This provides two main advantages:\n", + "- **Columnar Layout**: Arrow's memory layout is highly efficient for analytical queries and allows for **zero-copy** exchange with tools like **Polars**, **Pandas**, and **DuckDB**.\n", + "- **Native Speed**: By implementing performance-critical operations directly in Rust, we bypass the overhead of the Python interpreter, executing complex spatial transformations as compiled machine code." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The TrackingDataContainer" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Container created with 3 frames.\n" + ] + } + ], + "source": [ + "from datetime import datetime, timedelta, timezone\n", + "\n", + "import tracking_data_container as tdc\n", + "\n", + "# Create a small sample dataset from a dictionary\n", + "base_time = datetime(2023, 5, 27, 13, 30, 0, tzinfo=timezone.utc)\n", + "data = {\n", + " \"frame_id\": [1000, 1001, 1002],\n", + " \"timestamp\": [\n", + " base_time,\n", + " base_time + timedelta(milliseconds=40),\n", + " base_time + timedelta(milliseconds=80),\n", + " ],\n", + " \"period\": [\"first_half\", \"first_half\", \"first_half\"],\n", + " \"match/id\": [\"match_123\", \"match_123\", \"match_123\"],\n", + " \"ball/x\": [50.0, 51.0, 52.0],\n", + " \"ball/y\": [12.0, 13.0, 14.0],\n", + " \"teams/home/id\": [\"team_A\", \"team_A\", \"team_A\"],\n", + " \"teams/home/players/p1/x\": [23.5, 23.6, 23.7],\n", + " \"teams/home/players/p1/y\": [24.2, 24.3, 24.4],\n", + " \"teams/away/id\": [\"team_B\", \"team_B\", \"team_B\"],\n", + " \"teams/away/players/p2/x\": [33.1, 33.2, 33.3],\n", + " \"teams/away/players/p2/y\": [34.9, 35.0, 35.1],\n", + "}\n", + "\n", + "container = tdc.TrackingDataContainer(data)\n", + "print(f\"Container created with {container.num_rows()} frames.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Domain-Aware Operations\n", + "\n", + "We can easily map the data to any other coordinate system or normalize its orientation." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Original ball x at frame 0: 50.00%\n", + "Transformed ball x at frame 0: -0.00m\n", + "Orientation mode normalized to: static_home_away\n" + ] + } + ], + "source": [ + "# 1. Define the INITIAL coordinate system of our raw data (e.g. 0-100 percent)\n", + "raw_cs = tdc.CoordinateSystem(\n", + " x_range=(0, 100),\n", + " y_range=(0, 100),\n", + " origin=tdc.Origin.TopLeft,\n", + " length_unit=tdc.LengthUnit.Percent,\n", + ")\n", + "container = container.with_coordinate_system(raw_cs)\n", + "print(f\"Original ball x at frame 0: {container.materialize_frame(0)['ball/x']:.2f}%\")\n", + "\n", + "# 2. Define the TARGET coordinate system (in meters)\n", + "pitch_cs = tdc.CoordinateSystem(\n", + " x_range=(-52.5, 52.5),\n", + " y_range=(-34, 34),\n", + " origin=tdc.Origin.Center,\n", + " length_unit=tdc.LengthUnit.Meters,\n", + ")\n", + "\n", + "# 3. Map the data to this new system\n", + "transformed = container.transform_coordinates(pitch_cs)\n", + "print(f\"Transformed ball x at frame 0: {transformed.materialize_frame(0)['ball/x']:.2f}m\")\n", + "\n", + "# 4. Normalize orientation so teams are normalized based on their initial sides\n", + "normalized = transformed.with_orientation(tdc.OrientationMode.AwayHome).transform_orientation(\n", + " tdc.OrientationMode.StaticHomeAway\n", + ")\n", + "print(f\"Orientation mode normalized to: {normalized.orientation_mode}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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RsVrxDe/+c2YfB2pApK21NSvhx9+uDZ+/PjagQceWHvuued6/O7555+PP58wYULtkUce6fN11q9f3xV7+9FHH61xaHz961/v83n/+7//G5/3H//xHy2/9k9/+tPapk2bat3kne98Z+24447b5ucnnnhibcaMGbW1a9fWf/aVr3wl7pNrrrmm39ddt25d7YUXXojfsw/5/9inzbT6t3784x/Hn51//vn1n23YsKE2Z86c2jHHHFP/2R/90R/V3vzmN9f/zWc9ffr0+r9Xr15dmzp1au2yyy6rDaWDDz649sY3vrHWCfxd/v5Q+Id/+Ie4n2+99dYeP6cd+J3f+Z2WXuPVV1+tHXLIIbFtufnmm3v8bvPmzbVf/dVfjX/jX//1X/t8HT7f1157rdZNli9fHt/bt7/97dq0adNqf/EXf1ErKrbz9NNPb/o7zptmx0ER/cIv/ELt+OOP7/RmSF2P9qAxfE7Xl9tuuy2eZ+PGjat96EMfir+7/PLLa29/+9vjNXzHHXes7bPPPrW//Mu/jO18s9e47777am9605via8ycObP2mc98Zptt+MIXvlA76KCD4nMmTZpUO+KII2r/9E//FH/HNYjtyx/5dfd//ud/YkzDtWeXXXaJMc7999/f4/X//M//PP5/bMu73/3u+DcOO+yw+LtZs2bVTjrppBiv8HfHjh0br2UpfiFu4N9jxoypHX744bXbb799m+1funRp7V3veldt8uTJ8Xm8zne/+91tnnfvvffGWIW/sccee9T+6q/+qva1r30tbhuxal/SeyBO76v9/uEPf1j7wz/8w3gt4n3iscceiz/bf//949+eMmVK7dRTT93mb6bXuO6662of/OAHY9zEPv393//9GOsSS/3Wb/1WfF0eZ599dm3Lli09XoPr9wUXXBA/T/bFbrvtFv//F198sdafH/zgB/X30CyOJxacPXt27Zxzzunxe47Hd7zjHfFYIW5pdOWVV9aPkZ122ik+n88iaXaM5edEq+8pHUtXX311PAZ4Lv8fvv/979cWLlwY9yfbyGfx8Y9/vDYQJ5xwQm306NG1xx9/vKXnt7v9fP7z58+Pz2V/EyM2HiONjzze729/I31W3JNyT8DzfvEXf7HX9/AHf/AH8e8sWbKk6e9/9KMfxd/zvFbO+/S73CuvvBKP+1133TVuD8fUU089FZ/H8xv3QX7+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ccxoIA3NaI+0L/MKRlVD/pkz3ZFTsZSTybqSVNWlSVuZyHXatGmd3iQVyIEHHtgj4WSQNLCqAFZcShMmz5o1awg/IXW7/fbbr/79ww8/bHWdJP0fhmil62feuaHq2GWXXeqjEdavX99jSLQGXlVHvAGq+x0CW83pjtLnzr1d3imi8hhwso4SdhJDafgPXy1rH3mUlN933331fx9wwAFW/KgHhkWnIOnll1+OvS9qr3L1/vvv75H8tPdSOYZFp+Enr776anjggQfcQR1gXCKPg+JZtmxZj46NtPCVqmXu3Lk9jonXXnuto9vT7RhRlabdoMJqwoQJnd4kdQCVyumehErLDRs2+DmULP4c8BXzsMMOC4sWLaqvhshXfqaRb6zTXHVTp06NiRkpR4Nx0EEH9QiSnOC3vcrVF198MX7PKltOjK1mSOKmm9BHHnmkvjqbRo5xiTwOioVVsp977rn4/fjx461KrzA6jXfffff4PcP1nDJi4LjvSwuekaihUEPVxGifvLoun7JH5Yg/7d7qYgx9TSXQ3CQeeuihVtWpqSlTpoTp06fXS+fvvfde91QL2Fd33313j55hqwLUDInc/fffv16Nedddd7mgi6TKournzjvvrP+bhILXz2rLO44pNnCOrYFhRFWa0mafffaJw8tVXVQsjx49On7PEPPUQaJyMFnXpRpvBjlRGYol9WbevHn1xpyhsGkOGfXunnvuiQk7kOycMWOGu0u92nfffetDUZin6fHHH3dvSaokqvjTyA86DK1KF8PC0nHACA87tdq3YsWK+mJxzFmWz5mrauI4yBPhnFdpiLS6n8m6LkVFXRqax82hjbX6wxCUQw45pP5verwdDts7giGCIuywww7h9a9/vZWr6hPDUThO8t7vl156yb0mqVKIT5kOAFTTveENb/D6qejggw+uDxVbuXJleOqpp9wzAxztQSc88am099571+cnp5PE4bDlYbKuS+cAySe85+bQCe/VCiahTasFM/zg9ttvd6heEyzEkQ/fISBiXgipP5xfqXKAns1bbrnFpLikymDhgDvuuKPHfJ5MEyCBRB3T9uQjGBwO2z9GUpGoYxErMNIjLWwlMT85c6SlfMDy5cvDCy+84I4pAZN1XYZs+W233dZjDpCUfJFaaczp4U49cfRq5iu1aevQjB//+Mf1BAsB0Z577umuUcu4EUnT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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "fig, axes = plt.subplots(1, 2, figsize=(16, 5))\n", + "\n", + "# 1. Plot original frame (using 0-100% coordinate system)\n", + "container.plot_frame(0, ax=axes[0])\n", + "axes[0].set_title(\"Original Frame (0-100%, Top-Left Origin, AwayHome)\")\n", + "\n", + "# 2. Plot transformed frame (using meters coordinate system)\n", + "normalized.plot_frame(0, ax=axes[1])\n", + "axes[1].set_title(\"Transformed Frame (Meters, Center Origin, StaticHomeAway)\")\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The Common Data Format (CDF) Schema\n", + "\n", + "TDC uses the **Common Data Format (CDF)** standard for naming columns. This standard defines a hierarchical path structure (e.g., `teams/{team_id}/players/{player_id}/x`) that allows for standardized selection logic regardless of the data provider." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Selecting Columns\n", + "\n", + "TDC provides multiple ways to select columns, from metadata-aware selectors to simple string-based wildcards." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ball columns (using selector):\n", + "['ball/x', 'ball/y']\n", + "\n", + "Ball columns (using wildcard 'ball/**'):\n", + "['ball/x', 'ball/y']\n", + "\n", + "Player X columns:\n", + "['teams/home/players/p1/x', 'teams/away/players/p2/x']\n" + ] + } + ], + "source": [ + "# 1. Select using metadata selectors (e.g., all ball columns)\n", + "ball_data = container.select(container.selector.ball())\n", + "print(\"Ball columns (using selector):\")\n", + "print(ball_data.to_polars().columns)\n", + "\n", + "# 2. Select using recursive string wildcards\n", + "ball_data_wildcard = container.select(\"ball/**\")\n", + "print(\"\\nBall columns (using wildcard 'ball/**'):\")\n", + "print(ball_data_wildcard.to_polars().columns)\n", + "\n", + "# 3. Select all x-coordinates for all players\n", + "player_x = container.select(\"teams/*/players/*/x\")\n", + "print(\"\\nPlayer X columns:\")\n", + "print(player_x.to_polars().columns)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Schema Validation\n", + "\n", + "You can validate whether the data follows the CDF standard." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Schema validation failed: Missing ball/z\n" + ] + } + ], + "source": [ + "# Validate CDF compliance\n", + "try:\n", + " container.schema().validate()\n", + " print(\"Schema is valid CDF!\")\n", + "except tdc.exceptions.SchemaError as e:\n", + " print(f\"Schema validation failed: {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that you can still add columns that are not defined in the CDF." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Columns with custom metric: custom_metric\n" + ] + } + ], + "source": [ + "# Add a non-CDF column\n", + "container_with_custom = container.add_column(\"custom_metric\", [1.1, 2.2, 3.3])\n", + "print(f\"\\nColumns with custom metric: {container_with_custom.to_polars().columns[-1]}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Inspecting the Schema\n", + "\n", + "You can inspect the rich metadata associated with each column through the container's schema." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Column: frame_id | Entity: / | Attribute: frame_id\n", + "Column: timestamp | Entity: / | Attribute: timestamp\n", + "Column: period | Entity: / | Attribute: period\n", + "Column: match/id | Entity: / | Attribute: match\n", + "Column: ball/x | Entity: Ball | Attribute: x\n", + "Column: ball/y | Entity: Ball | Attribute: y\n", + "Column: teams/home/id | Entity: Team | Attribute: id\n", + "Column: teams/home/players/p1/x | Entity: Player | Attribute: x\n", + "Column: teams/home/players/p1/y | Entity: Player | Attribute: y\n", + "Column: teams/away/id | Entity: Team | Attribute: id\n" + ] + } + ], + "source": [ + "schema = container.schema()\n", + "for field in list(schema)[:10]: # Show first 10 columns\n", + " if field.entity:\n", + " print(\n", + " f\"Column: {field.name():<30} | Entity: {type(field.entity).__name__:<15} | Attribute: {field.attribute}\"\n", + " )\n", + " else:\n", + " print(f\"Column: {field.name():<30} | Entity: {'/':<15} | Attribute: {field.attribute}\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "/Users/probberechts/Developer/tracking_data_container", + "language": "python", + "name": "tracking_data_container" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.14" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/02-parsing.ipynb b/examples/02-parsing.ipynb new file mode 100644 index 0000000..f1e0962 --- /dev/null +++ b/examples/02-parsing.ipynb @@ -0,0 +1,267 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Efficient Parsing with TrackingDataBuilder\n", + "\n", + "Parsing raw tracking data (from XML, JSON, or CSV) is a computationally intensive task. While full-featured parsing is out of scope for the `tracking-data-container` project (this should be implemented by libraries such as [kloppy](https://github.com/py-sport/kloppy)), TDC provides optimized primitives for writing efficient parsers.\n", + "\n", + "By moving the parsing logic to Rust and providing the `TrackingDataBuilder` primitive, TDC achieves ingestion speeds that are often an order of magnitude faster than traditional Python-based approaches. Furthermore, the zero-copy conversion to Polars ensures that even large-scale tracking data can be moved into high-performance analysis environments with minimal latency." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## High-Performance Ingestion in Python\n", + "\n", + "The `TrackingDataBuilder` allows you to ingest data directly into Arrow memory. It handles internal column mapping and memory growth efficiently." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ingested 10000 frames in 0.0715 seconds.\n" + ] + } + ], + "source": [ + "import time\n", + "\n", + "import tracking_data_container as tdc\n", + "\n", + "builder = tdc.TrackingDataBuilder()\n", + "\n", + "\n", + "def custom_parser_simulator(builder, num_frames=10000):\n", + " for f_id in range(num_frames):\n", + " # Use .insert() for all types\n", + " builder.insert(f_id, \"ball/x\", 50.0)\n", + " builder.insert(f_id, \"ball/y\", 20.0)\n", + " builder.insert(f_id, \"ball_status\", True)\n", + "\n", + " for p_id in range(22):\n", + " builder.insert(f_id, f\"teams/home/players/p{p_id}/x\", 10.5)\n", + "\n", + "\n", + "start = time.perf_counter()\n", + "custom_parser_simulator(builder)\n", + "container = builder.build()\n", + "end = time.perf_counter()\n", + "\n", + "print(f\"Ingested {container.num_rows()} frames in {end - start:.4f} seconds.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Writing Parsers in Rust\n", + "\n", + "For the ultimate performance, parsers can be implemented in **Rust**. \n", + "Here is a more extensive example of how a Rust parser is structured using `Rayon` for parallelism:" + ] + }, + { + "cell_type": "raw", + "metadata": {}, + "source": [ + "/* \n", + "// Rust High-Performance Parser Pattern\n", + "use tdc_core::datacontainer::builder::TrackingDataBuilder;\n", + "use arrow::datatypes::DataType;\n", + "use rayon::prelude::*;\n", + "\n", + "pub fn parse_tracking_parallel(chunks: Vec) -> TrackingDataContainer {\n", + " chunks.into_par_iter().map(|chunk| {\n", + " let mut builder = TrackingDataBuilder::new();\n", + " \n", + " // Pre-resolve column IDs to avoid string hashing in the loop\n", + " let x_id = builder.get_or_create_column_id(\"ball/x\", DataType::Float64);\n", + " let y_id = builder.get_or_create_column_id(\"ball/y\", DataType::Float64);\n", + "\n", + " for raw_frame in chunk.frames {\n", + " let row = builder.get_row_index(raw_frame.id);\n", + " builder.insert_float_at_idx(row, x_id, raw_frame.x);\n", + " builder.insert_float_at_idx(row, y_id, raw_frame.y);\n", + " }\n", + " builder\n", + " }).reduce(TrackingDataBuilder::new, |mut a, b| {\n", + " // Highly optimized merge operation\n", + " a.merge(b);\n", + " a\n", + " }).build().unwrap()\n", + "}\n", + "*/" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Benchmark: Loading Data\n", + "\n", + "We compare the time taken to load a full Sportec (DFL) tracking XML file using a specialized Rust-based parser that implements the principles above." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "POS_FILE = \"data/DFL_04_03_positions_raw_observed_DFL-COM-000001_DFL-MAT-J03WMX.xml\"\n", + "META_FILE = \"data/DFL_02_01_matchinformation_DFL-COM-000001_DFL-MAT-J03WMX.xml\"\n", + "\n", + "# Ensure data is present\n", + "if not Path(POS_FILE).exists():\n", + " print(\"Downloading data for benchmark...\")\n", + " from huggingface_hub import hf_hub_download\n", + "\n", + " hf_hub_download(\n", + " repo_id=\"pysport/idsse-data\",\n", + " filename=\"DFL_02_01_matchinformation_DFL-COM-000001_DFL-MAT-J03WMX.xml\",\n", + " repo_type=\"dataset\",\n", + " local_dir=\"data\",\n", + " )\n", + " hf_hub_download(\n", + " repo_id=\"pysport/idsse-data\",\n", + " filename=\"DFL_04_03_positions_raw_observed_DFL-COM-000001_DFL-MAT-J03WMX.xml\",\n", + " repo_type=\"dataset\",\n", + " local_dir=\"data\",\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--- Loading benchmark ---\n", + "TDC (Rust): 1.18s\n", + "Fast-Forward: 1.60s\n", + "Kloppy: 19.38s\n" + ] + } + ], + "source": [ + "import time\n", + "\n", + "import tracking_data_container as tdc\n", + "from kloppy import sportec\n", + "import fastforward as fff\n", + "\n", + "\n", + "print(\"--- Loading benchmark ---\")\n", + "start = time.perf_counter()\n", + "tdc_container = tdc.load_sportec(POS_FILE, META_FILE)\n", + "tdc_load_time = time.perf_counter() - start\n", + "print(f\"TDC (Rust): {tdc_load_time:.2f}s\")\n", + "\n", + "start = time.perf_counter()\n", + "fff_dataset = fff.sportec.load_tracking(POS_FILE, META_FILE)\n", + "fff_load_time = time.perf_counter() - start\n", + "print(f\"Fast-Forward: {fff_load_time:.2f}s\")\n", + "\n", + "start = time.perf_counter()\n", + "kloppy_dataset = sportec.load_tracking(\n", + " raw_data=POS_FILE, meta_data=META_FILE, coordinates=\"sportec\"\n", + ")\n", + "kloppy_load_time = time.perf_counter() - start\n", + "print(f\"Kloppy: {kloppy_load_time:.2f}s\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Benchmark: Polars Conversion\n", + "\n", + "Once the data is loaded into memory, we often want to convert it to a Polars DataFrame for analysis. Because TDC is built on Arrow, this conversion is **zero-copy** and near-instant." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--- to_polars benchmark ---\n", + "TDC to_polars: 13.31ms\n", + "Fast-Forward tracking access: 0.02ms\n", + "Kloppy to_df(engine=\"polars\"): 1133.28ms\n", + "\n", + "Speedup TDC vs Kloppy: 85.2x\n" + ] + } + ], + "source": [ + "print(\"--- to_polars benchmark ---\")\n", + "\n", + "# 1. TDC to Polars (Zero-Copy)\n", + "start = time.perf_counter()\n", + "df_tdc = tdc_container.to_polars()\n", + "tdc_conv_time = time.perf_counter() - start\n", + "print(f\"TDC to_polars: {tdc_conv_time * 1000:.2f}ms\")\n", + "\n", + "# 2. Fast-Forward to Polars\n", + "start = time.perf_counter()\n", + "df_fff = fff_dataset.tracking \n", + "fff_conv_time = time.perf_counter() - start\n", + "print(f\"Fast-Forward tracking access: {fff_conv_time * 1000:.2f}ms\")\n", + "\n", + "# 3. Kloppy to Polars\n", + "start = time.perf_counter()\n", + "df_kloppy = kloppy_dataset.to_df(engine=\"polars\")\n", + "kloppy_conv_time = time.perf_counter() - start\n", + "print(f'Kloppy to_df(engine=\"polars\"): {kloppy_conv_time * 1000:.2f}ms')\n", + "print(f\"\\nSpeedup TDC vs Kloppy: {kloppy_conv_time / tdc_conv_time:.1f}x\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "/Users/probberechts/Developer/tracking_data_container", + "language": "python", + "name": "tracking_data_container" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.14" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/03-interop.ipynb b/examples/03-interop.ipynb new file mode 100644 index 0000000..c363521 --- /dev/null +++ b/examples/03-interop.ipynb @@ -0,0 +1,228 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Interoperability: Zero-Copy Data Exchange\n", + "\n", + "One of the core strengths of the `tracking-data-container` (TDC) is its ability to seamlessly exchange data with the broader Python data science ecosystem. TDC is built on **Apache Arrow**, a language-independent columnar memory format. Because Arrow defines a standardized way to represent data in memory, different libraries can share that data without the need for expensive \"serialization\" or \"copying\".\n", + "\n", + "This notebook illustrates how to convert a `TrackingDataContainer` to other popular formats like **Apache Arrow**, **Polars**, **Pandas**, and **Parquet**." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Container created with 3 rows.\n" + ] + } + ], + "source": [ + "from datetime import datetime, timedelta, timezone\n", + "\n", + "import tracking_data_container as tdc\n", + "\n", + "# Create a sample container\n", + "base_time = datetime(2023, 5, 27, 13, 30, 0, tzinfo=timezone.utc)\n", + "data = {\n", + " \"frame_id\": [1000, 1001, 1002],\n", + " \"period\": [\"first_half\", \"first_half\", \"first_half\"],\n", + " \"timestamp\": [\n", + " base_time,\n", + " base_time + timedelta(milliseconds=40),\n", + " base_time + timedelta(milliseconds=80),\n", + " ],\n", + " \"ball/x\": [50.0, 51.0, 52.0],\n", + " \"ball/y\": [12.0, 13.0, 14.0],\n", + " \"teams/home/players/p1/x\": [23.5, 23.6, 23.7],\n", + "}\n", + "cs = tdc.CoordinateSystem(\n", + " x_range=(0, 100),\n", + " y_range=(0, 100),\n", + " origin=tdc.Origin.TopLeft,\n", + " length_unit=tdc.LengthUnit.Percent,\n", + ")\n", + "container = tdc.TrackingDataContainer(data).with_coordinate_system(cs)\n", + "print(f\"Container created with {container.num_rows()} rows.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Apache Arrow\n", + "\n", + "You can extract the underlying data as a `pyarrow.RecordBatch`. This is the most \"raw\" representation of the data and is useful if you are working with other tools in the Arrow ecosystem." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Type: \n", + "Number of columns: 6\n" + ] + } + ], + "source": [ + "arrow_batch = container.to_arrow()\n", + "print(f\"Type: {type(arrow_batch)}\")\n", + "print(f\"Number of columns: {arrow_batch.num_columns}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Polars\n", + "\n", + "Polars is a blazingly fast DataFrame library also built on Apache Arrow. Converting to a Polars DataFrame is the recommended way to perform complex data manipulations in Python." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Type: \n", + "Polars DataFrame:\n", + "shape: (3, 6)\n", + "┌──────────┬────────────┬─────────────────────────┬────────┬────────┬─────────────────────────┐\n", + "│ frame_id ┆ period ┆ timestamp ┆ ball/x ┆ ball/y ┆ teams/home/players/p1/x │\n", + "│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │\n", + "│ u64 ┆ str ┆ datetime[μs] ┆ f32 ┆ f32 ┆ f32 │\n", + "╞══════════╪════════════╪═════════════════════════╪════════╪════════╪═════════════════════════╡\n", + "│ 1000 ┆ first_half ┆ 2023-05-27 13:30:00 ┆ 50.0 ┆ 12.0 ┆ 23.5 │\n", + "│ 1001 ┆ first_half ┆ 2023-05-27 13:30:00.040 ┆ 51.0 ┆ 13.0 ┆ 23.6 │\n", + "│ 1002 ┆ first_half ┆ 2023-05-27 13:30:00.080 ┆ 52.0 ┆ 14.0 ┆ 23.700001 │\n", + "└──────────┴────────────┴─────────────────────────┴────────┴────────┴─────────────────────────┘\n" + ] + } + ], + "source": [ + "df_polars = container.to_polars()\n", + "print(f\"Type: {type(df_polars)}\")\n", + "print(\"Polars DataFrame:\")\n", + "print(df_polars.head())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Pandas\n", + "\n", + "For traditional data science workflows, you can convert the container to a Pandas DataFrame. Note that while this is highly convenient, Pandas does not use Arrow memory internally, so a copy might be performed depending on the data types and your Pandas version." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Type: \n", + "Pandas DataFrame:\n", + " frame_id period timestamp ball/x ball/y \\\n", + "0 1000 first_half 2023-05-27 13:30:00.000 50.0 12.0 \n", + "1 1001 first_half 2023-05-27 13:30:00.040 51.0 13.0 \n", + "2 1002 first_half 2023-05-27 13:30:00.080 52.0 14.0 \n", + "\n", + " teams/home/players/p1/x \n", + "0 23.500000 \n", + "1 23.600000 \n", + "2 23.700001 \n" + ] + } + ], + "source": [ + "df_pandas = container.to_pandas()\n", + "print(f\"Type: {type(df_pandas)}\")\n", + "print(\"Pandas DataFrame:\")\n", + "print(df_pandas.head())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Parquet\n", + "\n", + "Parquet is a high-performance storage format designed for columnar data. TDC allows you to save your container directly to Parquet, keeping track of the coordinate system and orientation." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saved to ./data/match_data_sample.parquet\n", + "Reloaded container has 3 rows.\n", + "Reloaded container uses \n" + ] + } + ], + "source": [ + "parquet_file = \"./data/match_data_sample.parquet\"\n", + "container.to_parquet(parquet_file)\n", + "print(f\"Saved to {parquet_file}\")\n", + "\n", + "# You can also load it back directly into a container\n", + "reloaded = tdc.TrackingDataContainer.from_parquet(parquet_file)\n", + "print(f\"Reloaded container has {reloaded.num_rows()} rows.\")\n", + "print(f\"Reloaded container uses {reloaded.coordinate_system}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "/Users/probberechts/Developer/tracking_data_container", + "language": "python", + "name": "tracking_data_container" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.14" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/04-udfs.ipynb b/examples/04-udfs.ipynb new file mode 100644 index 0000000..9de72b3 --- /dev/null +++ b/examples/04-udfs.ipynb @@ -0,0 +1,315 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Computed Metrics and Performance\n", + "\n", + "Adding computed metrics, such as ball speed or player velocity, is a core part of tracking data analysis. The TDC supports multiple ways to efficiently add data, which enables interoperability between packages. This notebook compares different execution strategies for adding columns and benchmarks their performance." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded 145967 frames.\n" + ] + } + ], + "source": [ + "import time\n", + "\n", + "import polars as pl\n", + "import tracking_data_container as tdc\n", + "\n", + "# Load the sample data\n", + "POS_FILE = \"./data/DFL_04_03_positions_raw_observed_DFL-COM-000001_DFL-MAT-J03WMX.xml\"\n", + "META_FILE = \"./data/DFL_02_01_matchinformation_DFL-COM-000001_DFL-MAT-J03WMX.xml\"\n", + "\n", + "container = tdc.load_sportec(POS_FILE, META_FILE)\n", + "print(f\"Loaded {container.num_rows()} frames.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Execution Strategies\n", + "\n", + "TDC supports multiple ways to add computed columns, balancing ease-of-use with raw performance. We will add a velocity column for every player using four different methods:\n", + "\n", + "1. **TDC Expression Engine**: Uses the internal DataFusion engine. Flexible and supports lazy evaluation.\n", + "2. **Vectorized Python Functions**: Recommended for custom Python logic. Uses zero-copy Arrow exchange to perform calculations in a single batched pass.\n", + "3. **Optimized & 3rd-Party Rust Kernels**: High-performance Rust functions that operate directly on Arrow arrays via the Arrow C Data Interface.\n", + "4. **Native Polars Batch**: Converting the entire container to a Polars DataFrame for processing." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1. TDC Expression Engine\n", + "\n", + "The expression engine allows building logical plans that can be optimized and executed in Rust. While powerful, it currently has some overhead for eager per-player additions." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Expression Engine time: 0.5881s\n" + ] + } + ], + "source": [ + "start = time.perf_counter()\n", + "expr_fn = lambda x, y: (tdc.col(x).diff().pow(2.0) + tdc.col(y).diff().pow(2.0)).sqrt()\n", + "container_expr = container.add_column_per_player(\"vel\", expr_fn, on=[\"x\", \"y\"])\n", + "print(f\"Expression Engine time: {time.perf_counter() - start:.4f}s\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2. Vectorized Python Functions\n", + "\n", + "By passing a callable that operates on Arrow arrays (which can be converted to Polars or NumPy), we leverage TDC's zero-copy integration. This is often the fastest way to implement custom Python-based metrics because it avoids the quadratic cost of iterative updates." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Polars Vectorized time: 0.3581s\n" + ] + } + ], + "source": [ + "import polars as pl\n", + "\n", + "def compute_speed_polars(x, y):\n", + " # Inputs are pyarrow.Array objects\n", + " x, y = pl.Series(x), pl.Series(y)\n", + " return (x.diff().fill_null(0) ** 2 + y.diff().fill_null(0) ** 2).sqrt()\n", + "\n", + "start = time.perf_counter()\n", + "container_polars_vec = container.add_column_per_player(\"vel\", compute_speed_polars, on=[\"x\", \"y\"])\n", + "print(f\"Polars Vectorized time: {time.perf_counter() - start:.4f}s\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Numpy Vectorized time: 0.0208s\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "\n", + "def compute_speed_numpy(x, y):\n", + " x = x.to_numpy(zero_copy_only=False)\n", + " y = y.to_numpy(zero_copy_only=False)\n", + " return np.hypot(np.diff(x, prepend=x[0]), np.diff(y, prepend=y[0]))\n", + "\n", + "start = time.perf_counter()\n", + "container_numpy_vec = container.add_column_per_player(\"vel\", compute_speed_numpy, on=[\"x\", \"y\"])\n", + "print(f\"Numpy Vectorized time: {time.perf_counter() - start:.4f}s\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3. Optimized & 3rd-Party Rust Kernels (Arrow-Native)\n", + "\n", + "TDC supports the **Arrow C Data Interface** (via `__arrow_c_array__`), allowing you to use hand-optimized Rust kernels from any 3rd-party Rust library. These kernels operate directly on Arrow capsules, avoiding Python overhead.\n", + "\n", + "#### Python Usage\n", + "As an example, a few common metrics like `speed` are implemented as Rust kernels in the `tdc.ops` module. You can pass them directly to `add_column` or `add_column_per_player`." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Optimized Rust Kernel time: 0.0463s\n" + ] + } + ], + "source": [ + "start = time.perf_counter()\n", + "container_rust = container.add_column_per_player(\"vel\", tdc.ops.compute_speed, on=[\"x\", \"y\"])\n", + "print(f\"Optimized Rust Kernel time: {time.perf_counter() - start:.4f}s\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Implementing Your Own Kernel\n", + "\n", + "If you have a 3rd-party Rust library `my_rust_lib` that exposes a function taking and returning Arrow capsules, you can use it directly in TDC. A typical implementation using `pyo3` and `arrow-rs` would look like this:\n", + "\n", + "```rust\n", + "#[pyfunction]\n", + "fn compute_metric(py: Python<'_>, array: Bound<'_, PyAny>) -> PyResult {\n", + " // 1. Import from capsule (Zero-copy)\n", + " let (array_cap, schema_cap) = array.call_method0(\"__arrow_c_array__\")?\n", + " .extract::<(Bound<'_, PyCapsule>, Bound<'_, PyCapsule>)>()?;\n", + " let input_arr = pyo3_arrow::PyArray::from_arrow_pycapsule(&array_cap, &schema_cap)?;\n", + " let array = input_arr.into_inner().0;\n", + " \n", + " // 2. Vectorized calculation in Rust using arrow-rs\n", + " let result = /* ... logic ... */;\n", + " \n", + " // 3. Export back as capsule (Zero-copy)\n", + " Ok(pyo3_arrow::PyArray::new(result, Arc::new(field)).into_py(py))\n", + "}\n", + "```\n", + "\n", + "You can then use it in Python just like the built-in kernels:\n", + "\n", + "```python\n", + "import my_rust_lib\n", + "container.add_column(\"custom_metric\", my_rust_lib.compute_metric, on=[\"x\"])\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4. Native Polars Batch\n", + "\n", + "You can also convert the entire container to Polars, perform all transformations, and re-import it. The re-import is zero-copy using Arrow capsules." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Native Polars Batch time: 0.9378s\n" + ] + } + ], + "source": [ + "start = time.perf_counter()\n", + "df = container.to_polars()\n", + "exprs = []\n", + "for player in container.schema().players():\n", + " x = tdc.DataField(entity=player, attribute=\"x\").name()\n", + " y = tdc.DataField(entity=player, attribute=\"y\").name()\n", + " out = tdc.DataField(entity=player, attribute=\"vel\").name()\n", + " exprs.append(\n", + " (pl.col(x).diff().fill_null(0) ** 2 + pl.col(y).diff().fill_null(0) ** 2).sqrt().alias(out)\n", + " )\n", + "\n", + "df = df.with_columns(exprs)\n", + "container_polars_native = tdc.TrackingDataContainer(df)\n", + "print(f\"Native Polars Batch time: {time.perf_counter() - start:.4f}s\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Verification\n", + "\n", + "We verify that all execution strategies produce consistent results for a sample player." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Max difference Expr vs Polars: 0.0\n", + "Max difference Expr vs Numpy: 5.551115123125783e-17\n", + "Max difference Expr vs Rust: 0.0\n", + "\n", + "Verification Successful: All methods produced identical results.\n" + ] + } + ], + "source": [ + "p1 = container.schema().players()[0]\n", + "vel_col = tdc.DataField(entity=p1, attribute=\"vel\").name()\n", + "\n", + "v_expr = container_expr.to_polars()[vel_col].fill_null(0)\n", + "v_polars = container_polars_vec.to_polars()[vel_col].fill_null(0)\n", + "v_numpy = container_numpy_vec.to_polars()[vel_col].fill_null(0)\n", + "v_rust = container_rust.to_polars()[vel_col].fill_null(0)\n", + "\n", + "mismatch_polars = (v_expr - v_polars).abs().max()\n", + "mismatch_numpy = (v_expr - v_numpy).abs().max()\n", + "mismatch_rust = (v_expr - v_rust).abs().max()\n", + "\n", + "print(f\"Max difference Expr vs Polars: {mismatch_polars}\")\n", + "print(f\"Max difference Expr vs Numpy: {mismatch_numpy}\")\n", + "print(f\"Max difference Expr vs Rust: {mismatch_rust}\")\n", + "\n", + "if mismatch_polars < 1e-6 and mismatch_rust < 1e-6:\n", + " print(\"\\nVerification Successful: All methods produced identical results.\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "/Users/probberechts/Developer/tracking_data_container", + "language": "python", + "name": "tracking_data_container" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.14" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/05-lazy.ipynb b/examples/05-lazy.ipynb new file mode 100644 index 0000000..a489553 --- /dev/null +++ b/examples/05-lazy.ipynb @@ -0,0 +1,183 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Lazy Evaluation\n", + "\n", + "Loading massive datasets into memory is often unnecessary if you only need a small subset of the data. TDC supports **lazy evaluation** using **DataFusion**, allowing you to define complex queries that are optimized and executed only when needed." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You might wonder: *why does TDC implement its own lazy execution instead of relying on Polars and DuckDB?* \n", + "\n", + "1. **Metadata-Aware Optimization**: Our lazy engine can optimize selections based on the sports domain (e.g., \"only load coordinates for the active players\").\n", + "2. **Domain-Specific Pushdown**: Beyond simple filters, TDC can push down sports logic (like \"filter frames by ball-in-play status\") into the scanner, which generic engines cannot easily optimize without custom functions.\n", + "3. **Seamless Metadata Propagation**: When you perform a lazy query in TDC, the resulting eager container still has its schema, coordinate systems, and orientation metadata intact. Converting to Polars too early loses this domain context.\n", + "4. **Unified API**: You can use the same selectors (`container.selector.ball()`) and transformations whether you are working with an in-memory dataset or scanning 100GB of Parquet files.\n", + "\n", + "*(that aside, I think this should have low priority)*" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## What is Lazy Evaluation?\n", + "\n", + "In \"eager\" mode, every operation (like loading a file or adding a column) happens immediately. In \"lazy\" mode, these operations are recorded in a **logical plan**. This allows the engine to:\n", + "1. **Push down filters**: Only read the rows from a Parquet file that match your criteria.\n", + "2. **Prune columns**: Only load the columns required for your final result.\n", + "3. **Optimize memory**: Avoid allocating large buffers for intermediate steps." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "from pathlib import Path\n", + "\n", + "import tracking_data_container as tdc\n", + "\n", + "PARQUET_FILE = \"data/match_data.parquet\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Scanning Parquet Lazily\n", + "\n", + "The `scan_parquet` method returns a `LazyTrackingDataContainer`. This operation is instantaneous because it only reads the file metadata." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Lazy container initialized.\n" + ] + } + ], + "source": [ + "lazy_container = tdc.scan_parquet(PARQUET_FILE)\n", + "print(\"Lazy container initialized.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Defining a Query Plan\n", + "\n", + "You can chain filters and selections. Notice that no data processing happens yet. We can use the same powerful TDC selectors here." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Query plan defined.\n" + ] + } + ], + "source": [ + "# Define a query: only ball coordinates from the first half\n", + "query = (\n", + " lazy_container\n", + " .filter(tdc.col(\"period\").eq(tdc.lit(\"first_half\")))\n", + " .select([\"frame_id\", \"ball/**\"])\n", + ")\n", + "print(\"Query plan defined.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Executing the Plan\n", + "\n", + "To get the results, call `.collect()`. The internal engine will optimize the plan and return an eager `TrackingDataContainer` containing only the requested data." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Collected 1500 frames in 0.0050s.\n", + "shape: (5, 6)\n", + "┌──────────┬────────┬────────┬──────────┬────────┬──────────┐\n", + "│ frame_id ┆ ball/z ┆ ball/y ┆ ball/vel ┆ ball/x ┆ ball/acc │\n", + "│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │\n", + "│ i64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 │\n", + "╞══════════╪════════╪════════╪══════════╪════════╪══════════╡\n", + "│ 10000 ┆ 0.0 ┆ -0.17 ┆ 1.98 ┆ 0.18 ┆ 0.0 │\n", + "│ 10001 ┆ 0.0 ┆ -0.19 ┆ 2.02 ┆ -0.4 ┆ 0.0 │\n", + "│ 10002 ┆ 0.01 ┆ -0.21 ┆ 2.02 ┆ -0.97 ┆ 0.0 │\n", + "│ 10003 ┆ 0.01 ┆ -0.24 ┆ 2.02 ┆ -1.54 ┆ 0.0 │\n", + "│ 10004 ┆ 0.02 ┆ -0.26 ┆ 2.02 ┆ -2.12 ┆ 0.0 │\n", + "└──────────┴────────┴────────┴──────────┴────────┴──────────┘\n" + ] + } + ], + "source": [ + "start = time.perf_counter()\n", + "result = query.collect()\n", + "end = time.perf_counter()\n", + "\n", + "print(f\"Collected {result.num_rows()} frames in {end - start:.4f}s.\")\n", + "print(result.to_polars().head())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "/Users/probberechts/Developer/tracking_data_container", + "language": "python", + "name": "tracking_data_container" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.14" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a1a72dc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,120 @@ +[build-system] +requires = ["maturin>=1.10,<2.0"] +build-backend = "maturin" + +[project] +name = "tracking_data_container" +description = "A shared, standardized, metadata-aware container for sports tracking data." +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.9" +classifiers = [ + "Development Status :: 3 - Alpha", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Programming Language :: Rust", +] +dynamic = ["version"] + +[project.optional-dependencies] +all = [ + "pandas>=1.4.4", + "polars>=0.16.14", + "pyarrow>=8.0.0", +] +pandas = ["pandas>=1.4.4", "pyarrow>=8.0.0"] +polars = ["polars>=0.16.14"] +pyarrow = ["pyarrow>=8.0.0"] + +[dependency-groups] +dev = [ + "maturin>=1.7.0,<2.0", + "pandas>=1.4.4", + "polars>=0.16.14", + "pre-commit>=4.3.0", + "pyarrow>=8.0.0", +] +all = [ + { include-group = "benchmarks" }, + { include-group = "docs" }, + { include-group = "linting" }, + { include-group = "nb" }, + { include-group = "testing" }, +] +benchmarks = [ + "fast-forward-football>=0.1.1; python_version == '3.11'", + "kloppy>=3.15.0", + { include-group = "testing" }, +] +docs = ["pdoc", { include-group = "dev" }] +linting = [ + "mypy>=1.11.2,<2", + "pandas-stubs", + "ruff>=0.12,<0.15", + { include-group = "dev" }, +] +nb = [ + "huggingface-hub>=1.6.0", + "ipykernel", + "matplotlib>=3.5.3", + "mplsoccer>=0.1.1", +] +testing = [ + "pytest>=7.1.3", + "pytest-benchmark>=4.0.0,<6", + { include-group = "dev" }, +] + +[tool.maturin] +features = ["pyo3/extension-module"] +manifest-path = "crates/tdc-python/Cargo.toml" +module-name = "tracking_data_container._tracking_data_container" +python-source = "python" + +[tool.pytest.ini_options] +testpaths = "python/tests" +log_cli = true +log_cli_level = "INFO" + +[tool.ruff] +line-length = 100 +target-version = "py39" + +[tool.ruff.lint] +select = ["E", "F", "I", "Q", "FA102", "UP"] + +[tool.uv] +package = false +required-version = ">=0.8.4" + +[tool.mypy] +python_version = "3.9" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true +check_untyped_defs = true +exclude = ["python/tests"] + +[[tool.mypy.overrides]] +module = [ + "pyarrow.*", + "mplsoccer.*", + "kloppy.*", + "fastforward.*", + "fast_forward_football.*", + "matplotlib.*", + "scipy.*", + "pytest.*", + "_pytest.*", +] +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = "python.tests.*" +ignore_errors = true diff --git a/python/tests/benchmarks/conftest.py b/python/tests/benchmarks/conftest.py new file mode 100644 index 0000000..9568f40 --- /dev/null +++ b/python/tests/benchmarks/conftest.py @@ -0,0 +1,12 @@ +def pytest_benchmark_generate_json(config, benchmarks, include_stats=None): + """Add peak memory information to the benchmark and print a summary.""" + print("\n\n" + "=" * 25 + " Memory Usage Summary " + "=" * 25) + print(f"{'Benchmark':<60} {'Peak Memory (MB)':>15}") + print("-" * 76) + + for benchmark in benchmarks: + mem = benchmark.extra_info.get("peak_memory_mb", 0) + # Inject into stats metadata + benchmark.stats.metadata["peak_memory_mb"] = mem + print(f"{benchmark.name:<60} {mem:>15.1f}") + print("=" * 72 + "\n") diff --git a/python/tests/benchmarks/test_add_column_benchmark.py b/python/tests/benchmarks/test_add_column_benchmark.py new file mode 100644 index 0000000..fbfca9c --- /dev/null +++ b/python/tests/benchmarks/test_add_column_benchmark.py @@ -0,0 +1,156 @@ +import polars as pl +import pytest +import tracking_data_container as tdc + + +@pytest.fixture +def large_container(): + """ + Creates a large container (100k frames, 22 players) for realistic benchmarking. + """ + num_frames = 100000 + num_players = 22 + data = { + "frame_id": list(range(num_frames)), + "ball/x": [1.0] * num_frames, + "ball/y": [2.0] * num_frames, + } + for p in range(num_players): + data[f"teams/home/players/p{p}/x"] = [float(p)] * num_frames + data[f"teams/home/players/p{p}/y"] = [float(p)] * num_frames + + return tdc.TrackingDataContainer(data) + + +# --- Speed/Velocity Benchmarks --- + + +def test_speed_tdc_expression(benchmark, large_container): + """ + Benchmark: Adding velocity using the internal TDC expression engine (DataFusion). + This performs lazy evaluation and parallelization but has some overhead for small batches. + """ + + def run(): + def expr_fn(x, y): + return (tdc.col(x).diff().pow(2.0) + tdc.col(y).diff().pow(2.0)).sqrt() + + return large_container.add_column_per_player("vel", expr_fn, on=["x", "y"]) + + benchmark(run) + + +def test_speed_polars_vectorized(benchmark, large_container): + """ + Benchmark: Adding velocity using a Python function working on Polars Series. + Uses zero-copy Arrow exchange to pass data to Polars for vectorized calculation. + """ + + def compute_speed(x, y): + x = pl.Series(x) + y = pl.Series(y) + return (x.diff().fill_null(0) ** 2 + y.diff().fill_null(0) ** 2).sqrt() + + def run(): + return large_container.add_column_per_player("vel", compute_speed, on=["x", "y"]) + + benchmark(run) + + +def test_speed_rust_kernel(benchmark, large_container): + """ + Benchmark: Adding velocity using a hand-optimized Rust kernel. + Avoids all Python overhead and uses parallelized vectorized Arrow math. + """ + + def run(): + return large_container.add_column_per_player("vel", tdc.ops.compute_speed, on=["x", "y"]) + + benchmark(run) + + +def test_speed_constant_baseline(benchmark, large_container): + """ + Benchmark: Adding a constant pre-computed list. + Represents the baseline overhead for metadata updates and column additions. + """ + num_rows = large_container.num_rows() + values = [1.0] * num_rows + + def run(): + return large_container.add_column_per_player("constant", values) + + benchmark(run) + + +# --- Distance to Ball Benchmarks --- + + +def test_dist_tdc_expression(benchmark, large_container): + """ + Benchmark: Calculating distance to ball using TDC expressions. + Tests the engine's ability to handle interactions between player and global columns. + """ + + def run(): + def expr_fn(x, y): + return ( + (tdc.col(x) - tdc.col("ball/x")).pow(2.0) + + (tdc.col(y) - tdc.col("ball/y")).pow(2.0) + ).sqrt() + + return large_container.add_column_per_player("dist", expr_fn, on=["x", "y"]) + + benchmark(run) + + +def test_dist_polars_vectorized(benchmark, large_container): + """ + Benchmark: Calculating distance to ball using a vectorized Polars function. + Leverages zero-copy transfer to perform the spatial calculation in a single pass. + """ + + def compute_dist(x, y, bx, by): + x, y, bx, by = pl.Series(x), pl.Series(y), pl.Series(bx), pl.Series(by) + return ((x - bx) ** 2 + (y - by) ** 2).sqrt() + + def run(): + names = [] + values = [] + ons = [] + for p in large_container.schema().players(): + tid, pid = p.team_id, p.player_id + prefix = f"teams/{tid}/players/{pid}/" + names.append(f"{prefix}dist") + values.append(compute_dist) + ons.append([f"{prefix}x", f"{prefix}y", "ball/x", "ball/y"]) + + return large_container.add_columns(names, values, on=ons) + + benchmark(run) + + +def test_dist_polars_copy_loop(benchmark, large_container): + """ + Benchmark: Calculating distance to ball by manually looping and copying Polars DataFrames. + Demonstrates the performance penalty of iterative container construction (O(N^2)). + """ + + def run(): + df = pl.DataFrame(large_container) + for p in large_container.schema().players(): + tid, pid = p.team_id, p.player_id + prefix = f"teams/{tid}/players/{pid}/" + df = df.with_columns( + [ + ( + (pl.col(f"{prefix}x") - pl.col("ball/x")) ** 2 + + (pl.col(f"{prefix}y") - pl.col("ball/y")) ** 2 + ) + .sqrt() + .alias(f"{prefix}dist") + ] + ) + return tdc.TrackingDataContainer(df) + + benchmark(run) diff --git a/python/tests/benchmarks/test_lazy_filtering.py b/python/tests/benchmarks/test_lazy_filtering.py new file mode 100644 index 0000000..d2ae791 --- /dev/null +++ b/python/tests/benchmarks/test_lazy_filtering.py @@ -0,0 +1,85 @@ +import tracemalloc +from pathlib import Path + +import polars as pl +import pytest +import tracking_data_container as tdc + +STATSPERFORM_FILE = Path(__file__).parents[3] / "data" / "statsperform-ma25.txt" + + +@pytest.fixture(scope="session") +def statsperform_parquet(tmp_path_factory): + tmp_dir = tmp_path_factory.mktemp("data") + parquet_path = tmp_dir / "statsperform.parquet" + + # Load and save to parquet + container = tdc.load_statsperform(str(STATSPERFORM_FILE)) + container.to_parquet(parquet_path) + + return parquet_path + + +@pytest.mark.benchmark(group="lazy_filtering") +def test_lazy_filtering_tdc(benchmark, statsperform_parquet): + def run(): + # Lazy scan, filter for first half, and collect + return ( + tdc.scan_parquet(str(statsperform_parquet)) + .filter(tdc.col("period") == tdc.lit("first_half")) + .collect() + ) + + tracemalloc.start() + res = benchmark(run) + _, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + + peak_mb = peak / 1024 / 1024 + benchmark.extra_info["peak_memory_mb"] = peak_mb + + assert isinstance(res, tdc.TrackingDataContainer) + assert res.num_rows() > 0 + + +@pytest.mark.benchmark(group="lazy_filtering") +def test_lazy_filtering_tdc_selective(benchmark, statsperform_parquet): + def run(): + # Lazy scan, filter, SELECT, and collect + return ( + tdc.scan_parquet(str(statsperform_parquet)) + .filter(tdc.col("period") == tdc.lit("first_half")) + .select(["frame_id", "period", "ball/x", "ball/y"]) + .collect() + ) + + tracemalloc.start() + res = benchmark(run) + _, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + + peak_mb = peak / 1024 / 1024 + benchmark.extra_info["peak_memory_mb"] = peak_mb + + assert isinstance(res, tdc.TrackingDataContainer) + assert res.num_rows() > 0 + + +@pytest.mark.benchmark(group="lazy_filtering") +def test_lazy_filtering_polars(benchmark, statsperform_parquet): + def run(): + # Polars scan, filter, and collect + return ( + pl.scan_parquet(statsperform_parquet).filter(pl.col("period") == "first_half").collect() + ) + + tracemalloc.start() + res = benchmark(run) + _, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + + peak_mb = peak / 1024 / 1024 + benchmark.extra_info["peak_memory_mb"] = peak_mb + + assert isinstance(res, pl.DataFrame) + assert len(res) > 0 diff --git a/python/tests/benchmarks/test_sportec.py b/python/tests/benchmarks/test_sportec.py new file mode 100644 index 0000000..8841d50 --- /dev/null +++ b/python/tests/benchmarks/test_sportec.py @@ -0,0 +1,123 @@ +import tracemalloc +from pathlib import Path + +import polars as pl +import pytest +import tracking_data_container as tdc + +try: + from kloppy import sportec + + KLOPPY_AVAILABLE = True +except ImportError: + KLOPPY_AVAILABLE = False + +try: + import fastforward as fff + + # Try to access submodules to verify availability + _ = fff.sportec + FFF_AVAILABLE = True +except (ImportError, AttributeError): + FFF_AVAILABLE = False + +SPORTEC_FILE = Path(__file__).parents[3] / "data" / "sportec-tracking.xml" +SPORTEC_META = Path(__file__).parents[3] / "data" / "sportec-meta.xml" + + +def tdc_parse_sportec(path: Path) -> pl.DataFrame: + import xml.etree.ElementTree as ET + + builder = tdc.TrackingDataBuilder() + tree = ET.parse(path) + root = tree.getroot() + + for frame_set in root.findall(".//FrameSet"): + team_id = frame_set.get("TeamId") + person_id = frame_set.get("PersonId") + + if not team_id or not person_id: + continue + + col_x = f"teams/{team_id}/players/{person_id}/x" + col_y = f"teams/{team_id}/players/{person_id}/y" + + for frame in frame_set.findall("Frame"): + frame_n = int(frame.get("N")) + x = float(frame.get("X")) + y = float(frame.get("Y")) + + builder.insert(frame_n, col_x, x) + builder.insert(frame_n, col_y, y) + + return builder.build().to_polars() + + +@pytest.mark.benchmark(group="sportec") +def test_sportec_tdc_parsers(benchmark): + def run(): + return tdc.load_sportec(str(SPORTEC_FILE)).to_polars() + + tracemalloc.start() + df = benchmark(run) + _, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + + benchmark.extra_info["peak_memory_mb"] = peak / 1024 / 1024 + + assert isinstance(df, pl.DataFrame) + assert len(df) > 0 + + +@pytest.mark.benchmark(group="sportec") +def test_sportec_tdc_python(benchmark): + tracemalloc.start() + df = benchmark.pedantic( + tdc_parse_sportec, kwargs={"path": SPORTEC_FILE}, rounds=1, iterations=1 + ) + _, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + + benchmark.extra_info["peak_memory_mb"] = peak / 1024 / 1024 + + assert isinstance(df, pl.DataFrame) + assert len(df) > 0 + + +@pytest.mark.skipif(not KLOPPY_AVAILABLE, reason="kloppy not installed") +@pytest.mark.benchmark(group="sportec") +def test_sportec_kloppy(benchmark): + def run(): + dataset = sportec.load_tracking( + raw_data=str(SPORTEC_FILE), meta_data=str(SPORTEC_META), coordinates="sportec" + ) + return dataset.to_df(engine="polars") + + tracemalloc.start() + # Kloppy is slow, we use pedantic to limit rounds + df = benchmark.pedantic(run, rounds=1, iterations=1) + _, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + + benchmark.extra_info["peak_memory_mb"] = peak / 1024 / 1024 + + assert isinstance(df, pl.DataFrame) + assert len(df) > 0 + + +@pytest.mark.skipif(not FFF_AVAILABLE, reason="fastforward.sportec not available") +@pytest.mark.benchmark(group="sportec") +def test_sportec_fff(benchmark): + def run(): + dataset = fff.sportec.load_tracking(str(SPORTEC_FILE), str(SPORTEC_META)) + return dataset.tracking + + tracemalloc.start() + df = benchmark(run) + _, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + + benchmark.extra_info["peak_memory_mb"] = peak / 1024 / 1024 + + assert isinstance(df, pl.DataFrame) + assert len(df) > 0 diff --git a/python/tests/benchmarks/test_statsperform.py b/python/tests/benchmarks/test_statsperform.py new file mode 100644 index 0000000..11f4c3e --- /dev/null +++ b/python/tests/benchmarks/test_statsperform.py @@ -0,0 +1,153 @@ +import tracemalloc +from pathlib import Path + +import polars as pl +import pytest +import tracking_data_container as tdc + +try: + from kloppy import statsperform + + KLOPPY_AVAILABLE = True +except ImportError: + KLOPPY_AVAILABLE = False + +try: + import fastforward as fff + + _ = fff.statsperform + FFF_AVAILABLE = True +except (ImportError, AttributeError): + FFF_AVAILABLE = False + +STATSPERFORM_FILE = Path(__file__).parents[3] / "data" / "statsperform-ma25.txt" +STATSPERFORM_META = Path(__file__).parents[3] / "data" / "statsperform-ma1.json" + + +def tdc_parse_statsperform(path: Path) -> pl.DataFrame: + builder = tdc.TrackingDataBuilder() + + with open(path) as f: + for i, line in enumerate(f): + line = line.strip() + if not line: + continue + if i == 0 and ";" not in line: + continue + + parts = line.split(":") + frame_part = parts[0] + players_part = parts[1] + ball_part = parts[2] if len(parts) > 2 else None + + frame_id_str, info_str = frame_part.split(";") + frame_id = int(frame_id_str) + + if players_part != ";" and players_part: + for p_data in players_part.split(";"): + if not p_data: + continue + p_parts = p_data.split(",") + if not p_parts[0]: + continue + + team_side = int(p_parts[0]) + if team_side == 2: + continue # Referee + + team_id = "home" if team_side in (0, 3) else "away" + player_id = p_parts[1] + x = float(p_parts[3]) + y = float(p_parts[4]) + + builder.insert(frame_id, f"teams/{team_id}/players/{player_id}/x", x) + builder.insert(frame_id, f"teams/{team_id}/players/{player_id}/y", y) + + if ball_part: + b_info = ball_part.split(";")[0] + b_parts = b_info.split(",") + if len(b_parts) >= 2: + builder.insert(frame_id, "ball/x", float(b_parts[0])) + builder.insert(frame_id, "ball/y", float(b_parts[1])) + + return builder.build().to_polars() + + +@pytest.mark.benchmark(group="statsperform") +def test_statsperform_tdc_parsers(benchmark): + def run(): + return tdc.load_statsperform(str(STATSPERFORM_FILE)).to_polars() + + tracemalloc.start() + df = benchmark(run) + _, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + + peak_mb = peak / 1024 / 1024 + benchmark.extra_info["peak_memory_mb"] = peak_mb + + assert isinstance(df, pl.DataFrame) + assert len(df) > 0 + + +@pytest.mark.benchmark(group="statsperform") +def test_statsperform_tdc_python(benchmark): + tracemalloc.start() + df = benchmark.pedantic( + tdc_parse_statsperform, kwargs={"path": STATSPERFORM_FILE}, rounds=1, iterations=1 + ) + _, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + + peak_mb = peak / 1024 / 1024 + benchmark.extra_info["peak_memory_mb"] = peak_mb + + assert isinstance(df, pl.DataFrame) + assert len(df) > 0 + + +@pytest.mark.skipif(not KLOPPY_AVAILABLE, reason="kloppy not installed") +@pytest.mark.benchmark(group="statsperform") +def test_statsperform_kloppy(benchmark): + def run(): + dataset = statsperform.load_tracking( + ma1_data=str(STATSPERFORM_META), + ma25_data=str(STATSPERFORM_FILE), + coordinates="sportvu", + ) + return dataset.to_df(engine="polars") + + tracemalloc.start() + # Kloppy is slow, we use pedantic to limit rounds + df = benchmark.pedantic(run, rounds=1, iterations=1) + _, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + + peak_mb = peak / 1024 / 1024 + benchmark.extra_info["peak_memory_mb"] = peak_mb + + assert isinstance(df, pl.DataFrame) + assert len(df) > 0 + + +@pytest.mark.skipif(not FFF_AVAILABLE, reason="fastforward.statsperform not available") +@pytest.mark.benchmark(group="statsperform") +def test_statsperform_fff(benchmark): + def run(): + dataset = fff.statsperform.load_tracking( + ma1_data=str(STATSPERFORM_META), + ma25_data=str(STATSPERFORM_FILE), + ) + # Use .tracking which is the Polars DataFrame + return dataset.tracking + + tracemalloc.start() + df = benchmark(run) + _, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + + peak_mb = peak / 1024 / 1024 + benchmark.extra_info["peak_memory_mb"] = peak_mb + + assert isinstance(df, pl.DataFrame) + assert len(df) > 0 diff --git a/python/tests/conftest.py b/python/tests/conftest.py new file mode 100644 index 0000000..f991be9 --- /dev/null +++ b/python/tests/conftest.py @@ -0,0 +1,38 @@ +from datetime import datetime, timedelta, timezone + +import pytest +import tracking_data_container as tdc + + +@pytest.fixture +def sample_data(): + base_time = datetime(2023, 10, 1, 12, 0, 0, tzinfo=timezone.utc) + return { + "frame_id": [123456, 123457, 123458], + "timestamp": [ + base_time, + base_time + timedelta(milliseconds=40), + base_time + timedelta(milliseconds=80), + ], + "period": ["first_half", "first_half", "first_half"], + "match/id": ["match_12345", "match_12345", "match_12345"], + "ball_status": [True, True, True], + "teams/home/id": ["team_1", "team_1", "team_1"], + "teams/home/players/player_1/x": [23.5, 23.6, 23.7], + "teams/home/players/player_1/y": [24.2, 24.3, 24.4], + "teams/home/players/player_2/x": [56.3, 56.4, 56.5], + "teams/home/players/player_2/y": [12.8, 12.9, 13.0], + "teams/away/id": ["team_2", "team_2", "team_2"], + "teams/away/players/player_3/x": [33.1, 33.2, 33.3], + "teams/away/players/player_3/y": [34.9, 35.0, 35.1], + "teams/away/players/player_4/x": [45.6, 45.7, 45.8], + "teams/away/players/player_4/y": [24.0, 24.1, 24.2], + "ball/x": [50.0, 51.0, 52.0], + "ball/y": [12.0, 13.0, 14.0], + "ball/z": [0.5, 0.6, 0.7], + } + + +@pytest.fixture +def container(sample_data): + return tdc.TrackingDataContainer(sample_data) diff --git a/python/tests/test_add_column.py b/python/tests/test_add_column.py new file mode 100644 index 0000000..3dbc4a0 --- /dev/null +++ b/python/tests/test_add_column.py @@ -0,0 +1,69 @@ +import numpy as np +import tracking_data_container as tdc + + +class TestAddColumn: + def test_add_column_array(self, container): + data = [1.0, 2.0, 3.0] + container = container.add_column("ball/new", data) + assert "ball/new" in container.schema().names() + assert container.to_arrow().column("ball/new").to_pylist() == data + + def test_add_column_expr(self, container): + expr = (tdc.col("ball/x") + tdc.col("ball/y")).sqrt() + container = container.add_column("ball/dist", expr) + assert "ball/dist" in container.schema().names() + + # ball/x: [50.0, 51.0, 52.0], ball/y: [12.0, 13.0, 14.0] + expected = np.sqrt(np.array([50.0, 51.0, 52.0]) + np.array([12.0, 13.0, 14.0])) + actual = container.to_arrow().column("ball/dist").to_numpy() + np.testing.assert_allclose(actual, expected) + + def test_add_column_udf(self, container): + def my_udf(x): + return x.to_numpy() + 10.0 + + container = container.add_column("ball/x_plus_10", my_udf, on=["ball/x"]) + assert "ball/x_plus_10" in container.schema().names() + + # ball/x: [50.0, 51.0, 52.0] + expected = np.array([60.0, 61.0, 62.0]) + actual = container.to_arrow().column("ball/x_plus_10").to_numpy() + np.testing.assert_allclose(actual, expected) + + def test_add_column_ops(self, container): + # Test the tdc.ops.compute_speed rust kernel + container = container.add_column( + "ball/speed", tdc.ops.compute_speed, on=["ball/x", "ball/y"] + ) + assert "ball/speed" in container.schema().names() + + # x: [50.0, 51.0, 52.0], y: [12.0, 13.0, 14.0] + # dx=1, dy=1 -> speed=sqrt(2) approx 1.414 + expected = np.array([0.0, np.sqrt(2), np.sqrt(2)]) + actual = container.to_arrow().column("ball/speed").to_numpy() + np.testing.assert_allclose(actual, expected, atol=1e-6) + + def test_add_column_per_player_udf(self, container): + def compute_pos_sum(x, y): + return x.to_numpy() + y.to_numpy() + + container = container.add_column_per_player("pos_sum", compute_pos_sum, on=["x", "y"]) + assert "teams/home/players/player_1/pos_sum" in container.schema().names() + + # home player_1 x: [23.5, 23.6, 23.7], y: [24.2, 24.3, 24.4] + expected = np.array([23.5 + 24.2, 23.6 + 24.3, 23.7 + 24.4]) + actual = container.to_arrow().column("teams/home/players/player_1/pos_sum").to_numpy() + np.testing.assert_allclose(actual, expected) + + def test_add_column_per_player_ops(self, container): + container = container.add_column_per_player( + "speed_rust", tdc.ops.compute_speed, on=["x", "y"] + ) + assert "teams/home/players/player_1/speed_rust" in container.schema().names() + + # home player_1 x: [23.5, 23.6, 23.7], y: [24.2, 24.3, 24.4] + # dx=0.1, dy=0.1 -> speed=sqrt(0.01 + 0.01) = sqrt(0.02) approx 0.1414 + expected = np.array([0.0, np.sqrt(0.02), np.sqrt(0.02)]) + actual = container.to_arrow().column("teams/home/players/player_1/speed_rust").to_numpy() + np.testing.assert_allclose(actual, expected, atol=1e-6) diff --git a/python/tests/test_build.py b/python/tests/test_build.py new file mode 100644 index 0000000..527dcbb --- /dev/null +++ b/python/tests/test_build.py @@ -0,0 +1,93 @@ +from datetime import datetime, timedelta, timezone + +import tracking_data_container as tdc + + +class TestBuilder: + def test_tracking_data_builder(self) -> None: + builder = tdc.TrackingDataBuilder() + + base_time = datetime(2023, 10, 1, 12, 0, 0, tzinfo=timezone.utc) + ts1 = int(base_time.timestamp() * 1_000_000) + ts2 = int((base_time + timedelta(milliseconds=40)).timestamp() * 1_000_000) + + # Insert data for frame 1 + builder.insert(1, "frame_id", 1) + builder.insert(1, "timestamp", ts1) + builder.insert(1, "ball/x", 10) + builder.insert(1, "ball/y", 20.5) + builder.insert(1, "period", "first_half") + + # Insert data for frame 2 + builder.insert(2, "frame_id", 2) + builder.insert(2, "timestamp", ts2) + builder.insert(2, "ball/x", 15) + builder.insert(2, "ball/y", 25.5) + builder.insert(2, "period", "first_half") + + container = builder.build() + assert container.num_rows() == 2 + + # 1. Materialize by index + frame_0 = container.materialize_frame(0) + assert frame_0[tdc.DataField("ball/x")] == 10 + assert frame_0["ball/x"] == 10 + assert frame_0[tdc.DataField("ball/y")] == 20.5 + assert frame_0["ball/y"] == 20.5 + assert frame_0[tdc.DataField("period")] == "first_half" + assert frame_0["period"] == "first_half" + + # Check to_dict and repr + assert repr(frame_0) == "" + d = frame_0.to_dict() + assert d["frame_id"] == 1 + assert d["ball/x"] == 10 + assert d["ball/y"] == 20.5 + assert d["period"] == "first_half" + + # 2. Materialize by datetime + frame_ts = container.materialize_frame(base_time + timedelta(milliseconds=40)) + assert frame_ts[tdc.DataField("ball/x")] == 15 + + # 3. Materialize by timedelta + frame_td = container.materialize_frame(timedelta(milliseconds=0)) + assert frame_td[tdc.DataField("ball/x")] == 10 + + def test_tracking_data_builder_merge(self) -> None: + base_time = datetime(2023, 10, 1, 12, 0, 0, tzinfo=timezone.utc) + ts1 = int(base_time.timestamp() * 1_000_000) + ts2 = int((base_time + timedelta(milliseconds=40)).timestamp() * 1_000_000) + + # Builder 1 with some data + b1 = tdc.TrackingDataBuilder() + b1.insert(1, "frame_id", 1) + b1.insert(1, "timestamp", ts1) + b1.insert(1, "ball/x", 10) + + # Builder 2 with overlapping and new data + b2 = tdc.TrackingDataBuilder() + b2.insert(1, "frame_id", 1) + b2.insert(1, "timestamp", ts1) + b2.insert(1, "ball/y", 20) # same frame as b1 + b2.insert(2, "frame_id", 2) + b2.insert(2, "timestamp", ts2) + b2.insert(2, "ball/x", 30) # new frame + + # Test DataField as column + field = tdc.DataField("ball/z") + b2.insert(2, field, 40.5) + + b1.merge(b2) + + container = b1.build() + assert container.num_rows() == 2 + + # Frame 1 should have x and y + f1 = container.materialize_frame(timedelta(seconds=0)) + assert f1["ball/x"] == 10 # type: ignore + assert f1["ball/y"] == 20 # type: ignore + + # Frame 2 should have x and z + f2 = container.materialize_frame(1) + assert f2["ball/x"] == 30 # type: ignore + assert f2["ball/z"] == 40.5 # type: ignore diff --git a/python/tests/test_container.py b/python/tests/test_container.py new file mode 100644 index 0000000..0615ab0 --- /dev/null +++ b/python/tests/test_container.py @@ -0,0 +1,159 @@ +import pytest +import tracking_data_container as tdc + + +class TestBasics: + def test_container_init(self, container) -> None: + assert isinstance(container, tdc.TrackingDataContainer) + assert container.num_rows() == 3 + # In conftest.py: 5 ball cols, 8 player cols, frame_id, timestamp, period, + # match/id, ball_status, teams/home/id, teams/away/id + # Total = 5+8+7 = 20. But let's just check it's >= 18 + assert len(container.schema()) >= 18 + + def test_data_field_init(self) -> None: + field = tdc.DataField("ball/x") + assert field.entity.is_ball + assert field.attribute == "x" + assert field.name() == "ball/x" + + def test_orientation_mode(self, container) -> None: + # Default container has no orientation set + assert container.orientation_mode is None + + # Set orientation + container = container.with_orientation(tdc.OrientationMode.HomeAway) + assert container.orientation_mode == tdc.OrientationMode.HomeAway + + +class TestColumnAddition: + def test_container_add_column_direct(self, container) -> None: + new_values = [1.0, 2.0, 3.0] + container = container.add_column("test/new_col", new_values) + assert "test/new_col" in container.to_polars().columns + assert container.to_polars()["test/new_col"].to_list() == new_values + + def test_container_add_column_callable(self, container) -> None: + def compute_sum(frame): + return frame["ball/x"].to_numpy() + frame["ball/y"].to_numpy() + + container = container.add_column("ball/sum", compute_sum) + assert "ball/sum" in container.to_polars().columns + # Samples: [50.0, 51.0, 52.0] + [12.0, 13.0, 14.0] = [62.0, 64.0, 66.0] + res = container.to_polars()["ball/sum"].to_list() + assert res[0] == pytest.approx(62.0) + assert res[1] == pytest.approx(64.0) + assert res[2] == pytest.approx(66.0) + + def test_container_add_column_callable_on(self, container) -> None: + def compute_sum(x, y): + return x.to_numpy() + y.to_numpy() + + container = container.add_column("ball/sum_on", compute_sum, on=["ball/x", "ball/y"]) + assert "ball/sum_on" in container.to_polars().columns + res = container.to_polars()["ball/sum_on"].to_list() + assert res[0] == pytest.approx(62.0) + assert res[1] == pytest.approx(64.0) + assert res[2] == pytest.approx(66.0) + + def test_container_add_column_replace(self, container) -> None: + # Replacing an existing column + new_x = [100.0, 200.0, 300.0] + container = container.add_column("ball/x", new_x) + assert container.to_polars()["ball/x"].to_list() == pytest.approx(new_x) + + +class TestLazy: + def test_container_lazy_collect(self, container) -> None: + lazy_c = container.lazy() + assert isinstance(lazy_c, tdc.LazyTrackingDataContainer) + + # Collect back to eager + eager_c = lazy_c.collect() + assert isinstance(eager_c, tdc.TrackingDataContainer) + assert eager_c.num_rows() == container.num_rows() + + def test_container_lazy_ops(self, container) -> None: + lazy_c = container.lazy() + + # 1. Lazy select + subset_lazy = lazy_c.select("ball/x") + assert isinstance(subset_lazy, tdc.LazyTrackingDataContainer) + assert len(subset_lazy.schema()) == 1 + + # 2. Lazy add column (raw arrays) + new_values = [1.0, 2.0, 3.0] + with pytest.raises(tdc.exceptions.TrackingDataContainerError, match="not yet supported"): + lazy_c.add_column("test/lazy", new_values) + + def test_container_lazy_add_column_callable(self, container) -> None: + lazy_c = container.lazy() + + def compute_sum(x, y): + import polars as pl + + return pl.Series(x.to_numpy() + y.to_numpy()) + + lazy_c = lazy_c.add_column("ball/sum", compute_sum, on=["ball/x", "ball/y"]) + res = lazy_c.collect() + + assert "ball/sum" in res.to_polars().columns + res_list = res.to_polars()["ball/sum"].to_list() + assert res_list[0] == pytest.approx(62.0) + assert res_list[1] == pytest.approx(64.0) + assert res_list[2] == pytest.approx(66.0) + + def test_container_lazy_true_lazy_ops(self, container) -> None: + # Test things that ARE supported lazily (expressions) + lazy_c = container.lazy() + + # Expression-based addition + expr = tdc.col("ball/x").mul(2) + lazy_c = lazy_c.add_column("ball/x2", expr) + + # Filter (use threshold that definitely excludes some rows) + # Sample ball/x are [50.0, 51.0, 52.0] + lazy_c = lazy_c.filter(tdc.col("ball/x").gt(51.5)) + + # Collect + res = lazy_c.collect() + assert "ball/x2" in res.to_polars().columns + assert res.num_rows() == 1 + + def test_container_lazy_transformations(self, container) -> None: + # 1. Lazy transform coordinates + sys = tdc.CoordinateSystem( + x_range=(0, 100), y_range=(0, 50), length_unit="meters", origin="top_left" + ) + container = container.with_coordinate_system(sys) + + target_sys = tdc.CoordinateSystem( + x_range=(-50, 50), y_range=(-25, 25), length_unit="meters", origin="center" + ) + + lazy_c = container.lazy().transform_coordinates(target_sys) + res = lazy_c.collect() + + # Input ball/x was [50.0, 51.0, 52.0] in (0, 100) range + # Output should be in (-50, 50) range -> [0.0, 1.0, 2.0] + assert res.to_polars()["ball/x"].to_list() == pytest.approx([0.0, 1.0, 2.0]) + + # 2. Lazy transform orientation + # Setup container with known state + container = container.with_orientation(tdc.OrientationMode.HomeAway) + # Period values in sample data: ['first_half', 'second_half', 'first_half'] + # HomeAway means flip in second half. + # Flip across center of (0, 100) range: x -> 100 - x + lazy_c = container.lazy().transform_orientation(tdc.OrientationMode.AwayHome) + res = lazy_c.collect() + + # AwayHome means flip in first half. + # We changed from HomeAway (flip 2nd) to AwayHome (flip 1st). + # So frames 0, 1, 2: + # Frame 0: first_half. HomeAway (no flip) -> AwayHome (flip). CHANGE. + # Frame 1: second_half. HomeAway (flip) -> AwayHome (no flip). CHANGE. + # Frame 2: first_half. HomeAway (no flip) -> AwayHome (flip). CHANGE. + # All frames should be flipped. + assert res.to_polars()["ball/x"][0] == pytest.approx(100.0 - 50.0) + assert res.to_polars()["ball/x"][1] == pytest.approx(100.0 - 51.0) + assert res.to_polars()["ball/x"][2] == pytest.approx(100.0 - 52.0) diff --git a/python/tests/test_errors.py b/python/tests/test_errors.py new file mode 100644 index 0000000..71ba5ce --- /dev/null +++ b/python/tests/test_errors.py @@ -0,0 +1,15 @@ +import pytest +import tracking_data_container as tdc + + +@pytest.mark.parametrize( + "exc_class, expected_docstring", + [ + (tdc.exceptions.TrackingDataContainerError, "The base class for all errors"), + (tdc.exceptions.InvalidParametersError, "Provided parameters are invalid"), + (tdc.exceptions.SchemaError, "Schema validation failed"), + (tdc.exceptions.SchemaWarning, "Schema validation warning"), + ], +) +def test_docstrings(exc_class: type[Exception], expected_docstring: str) -> None: + assert exc_class.__doc__ == expected_docstring diff --git a/python/tests/test_interop.py b/python/tests/test_interop.py new file mode 100644 index 0000000..442b284 --- /dev/null +++ b/python/tests/test_interop.py @@ -0,0 +1,61 @@ +import pandas as pd +import polars as pl +import pyarrow as pa +import tracking_data_container as tdc + + +def test_to_arrow(container): + batch = container.to_arrow() + assert isinstance(batch, pa.RecordBatch) + assert batch.num_rows == 3 + assert batch.num_columns == 18 + assert batch.column(batch.schema.get_field_index("ball/x")).to_pylist() == [50.0, 51.0, 52.0] + assert batch.column(batch.schema.get_field_index("ball/y")).to_pylist() == [12.0, 13.0, 14.0] + + +def test_to_pandas(container): + df = container.to_pandas() + assert isinstance(df, pd.DataFrame) + assert len(df) == 3 + assert "ball/x" in df.columns + assert "ball/y" in df.columns + assert df["ball/x"].tolist() == [50.0, 51.0, 52.0] + + +def test_to_polars(container): + df = container.to_polars() + assert isinstance(df, pl.DataFrame) + assert df.height == 3 + assert "ball/x" in df.columns + assert "ball/y" in df.columns + assert df["ball/x"].to_list() == [50.0, 51.0, 52.0] + + +def test_zero_copy_polars(container): + # Polars can consume the container directly via the PyCapsule interface + df = pl.DataFrame(container) + assert df.height == 3 + assert df["ball/x"].to_list() == [50.0, 51.0, 52.0] + + +def test_parquet_roundtrip(container, tmp_path): + path = tmp_path / "test.parquet" + + # 1. Write to parquet + container.to_parquet(path) + assert path.exists() + + # 2. Read back eager + c2 = tdc.TrackingDataContainer.from_parquet(path) + assert c2.schema() == container.schema() + assert len(c2.to_arrow()) == 3 + + # 3. Scan lazy + lazy_c = tdc.scan_parquet(str(path)) + assert isinstance(lazy_c, tdc.LazyTrackingDataContainer) + assert lazy_c.schema() == container.schema() + + # 4. Collect and verify + c3 = lazy_c.collect() + assert c3.schema() == container.schema() + assert c3.to_polars().equals(container.to_polars()) diff --git a/python/tests/test_ops.py b/python/tests/test_ops.py new file mode 100644 index 0000000..48bd51a --- /dev/null +++ b/python/tests/test_ops.py @@ -0,0 +1,73 @@ +import numpy as np +import pyarrow as pa +import pytest +import tracking_data_container as tdc + + +def test_compute_derivatives(): + data = { + "ball/x": [0.0, 1.0, 3.0, 6.0], + "ball/y": [0.0, 0.0, 0.0, 0.0], + } + container = tdc.TrackingDataContainer(data) + df = container.to_polars() + + # 1. Speed (dt=1.0) + # displacements: 1.0, 2.0, 3.0 + # speed: 0.0, 1.0, 2.0, 3.0 + x_arr = pa.array(df["ball/x"]) + y_arr = pa.array(df["ball/y"]) + speed = tdc.ops.compute_speed(x_arr, y_arr, dt=1.0) + speed_vals = pa.array(speed).to_pylist() + assert speed_vals == [0.0, 1.0, 2.0, 3.0] + + # 2. Acceleration (dt=1.0) + # speed diffs: 1.0, 1.0, 1.0 + # acc: 0.0, 1.0, 1.0, 1.0 + acc = tdc.ops.compute_acceleration(speed, dt=1.0) + acc_vals = pa.array(acc).to_pylist() + assert acc_vals == [0.0, 1.0, 1.0, 1.0] + + # 3. Jerk (dt=1.0) + # acc diffs: 1.0, 0.0, 0.0 + # jerk: 0.0, 1.0, 0.0, 0.0 + jerk = tdc.ops.compute_jerk(acc, dt=1.0) + jerk_vals = pa.array(jerk).to_pylist() + assert jerk_vals == [0.0, 1.0, 0.0, 0.0] + + +def test_compute_angular_velocity(): + # Velocity vector rotating 90 degrees (PI/2) + vx = pa.array([1.0, 0.0]) + vy = pa.array([0.0, 1.0]) + + # dt = 1.0 + # theta: 0, PI/2 + # d_theta: PI/2 + # omega: PI/2 + omega = tdc.ops.compute_angular_velocity(vx, vy, dt=1.0) + omega_vals = pa.array(omega).to_pylist() + assert omega_vals[1] == pytest.approx(np.pi / 2) + + +def test_butterworth_filter(): + import pyarrow as pa + + # Constant signal with noise + data = [10.0] * 50 + # Add some noise + for i in range(len(data)): + if i % 2 == 0: + data[i] += 0.5 + else: + data[i] -= 0.5 + + arr = pa.array(data) + + # Apply filter (low cutoff) + filtered = tdc.ops.butterworth_filter(arr, cutoff=1.0, fs=25.0) + filtered_vals = pa.array(filtered).to_pylist() + + assert len(filtered_vals) == len(data) + # The filter takes some time to settle, check at the end + assert abs(filtered_vals[-1] - 10.0) < 0.1 diff --git a/python/tests/test_schema.py b/python/tests/test_schema.py new file mode 100644 index 0000000..fdd5491 --- /dev/null +++ b/python/tests/test_schema.py @@ -0,0 +1,303 @@ +import pytest +import tracking_data_container as tdc + + +class TestDataField: + def test_init_path(self) -> None: + field = tdc.DataField("teams/home/players/1/x") + assert field.name() == "teams/home/players/1/x" + + def test_init_components_player(self) -> None: + field = tdc.DataField(entity=tdc.Player("home", "10"), attribute="x") + assert field.name() == "teams/home/players/10/x" + + def test_init_components_ball(self) -> None: + field = tdc.DataField(entity=tdc.Ball(), attribute="z") + assert field.name() == "ball/z" + + def test_init_components_team(self) -> None: + field = tdc.DataField(entity=tdc.Team("away"), attribute="formation") + assert field.name() == "teams/away/formation" + + def test_init_components_frame(self) -> None: + field = tdc.DataField(attribute="timestamp") + assert field.name() == "timestamp" + + def test_init_vendor(self) -> None: + field = tdc.DataField("vendor/event/name") + assert field.name() == "vendor/event/name" + + field = tdc.DataField(attribute="vendor_event_name") + assert field.name() == "vendor/event/name" + + def test_equality(self) -> None: + f1 = tdc.DataField("ball/x") + f2 = tdc.DataField(entity=tdc.Ball(), attribute="x") + f3 = tdc.DataField("ball/y") + + assert f1 == f2 + assert f1 != f3 + + def test_datafield_subclasses(self) -> None: + frame_field = tdc.DataField(attribute="timestamp") + assert isinstance(frame_field, tdc.FrameDataField) + assert isinstance(frame_field, tdc.DataField) + assert frame_field.attribute == "timestamp" + + entity_field = tdc.DataField(entity=tdc.Player("home", "10"), attribute="x") + assert isinstance(entity_field, tdc.EntityDataField) + assert isinstance(entity_field, tdc.DataField) + assert entity_field.attribute == "x" + assert entity_field.team_id == "home" + assert entity_field.player_id == "10" + + ball_field = tdc.DataField(entity=tdc.Ball(), attribute="z") + assert isinstance(ball_field, tdc.EntityDataField) + assert ball_field.entity.is_ball is True + assert ball_field.attribute == "z" + + def test_tracked_entity_object(self) -> None: + field = tdc.DataField(entity=tdc.Player("away", "7"), attribute="y") + entity = field.entity + + assert isinstance(entity, tdc.Player) + assert isinstance(entity, tdc.TrackedEntity) + assert entity.team_id == "away" + assert entity.player_id == "7" + assert "Player(team=away, id=7)" in repr(entity) + + def test_ball_entity(self) -> None: + field = tdc.DataField(entity=tdc.Ball(), attribute="x") + entity = field.entity + + assert isinstance(entity, tdc.Ball) + assert isinstance(entity, tdc.TrackedEntity) + assert repr(entity) == "Ball" + + def test_team_entity(self) -> None: + field = tdc.DataField(entity=tdc.Team("home"), attribute="formation") + entity = field.entity + + assert isinstance(entity, tdc.Team) + assert isinstance(entity, tdc.TrackedEntity) + assert entity.team_id == "home" + assert "Team(id=home)" in repr(entity) + + def test_referee_entity(self) -> None: + field = tdc.DataField(entity=tdc.Referee("ref_1"), attribute="x") + entity = field.entity + + assert isinstance(entity, tdc.Referee) + assert isinstance(entity, tdc.TrackedEntity) + assert entity.referee_id == "ref_1" + assert "Referee(id=ref_1)" in repr(entity) + + def test_new_constructor(self) -> None: + player = tdc.Player(team_id="home", player_id="10") + field = tdc.DataField(entity=player, attribute="x") + assert field.name() == "teams/home/players/10/x" + assert field.entity == player + + ball = tdc.Ball() + field = tdc.DataField(entity=ball, attribute="z") + assert field.name() == "ball/z" + assert field.entity == ball + + # Test frame attribute with 'attribute' name + field = tdc.DataField(attribute="timestamp") + assert field.name() == "timestamp" + assert isinstance(field, tdc.FrameDataField) + assert field.entity is None + + def test_frame_field_entity_none(self) -> None: + field = tdc.DataField("frame_id") + assert field.entity is None + assert field.team_id is None + assert field.player_id is None + + +class TestFields: + def test_init(self) -> None: + fields = tdc.Fields(["frame_id", "ball/x", "teams/home/players/1/x"]) + assert len(fields) == 3 + assert fields[0].name() == "frame_id" + assert fields[2].name() == "teams/home/players/1/x" + + def test_iteration(self) -> None: + fields = tdc.Fields(["ball/x", "ball/y"]) + names = [f.name() for f in fields] + assert names == ["ball/x", "ball/y"] + + def test_indexing(self) -> None: + fields = tdc.Fields(["ball/x", "ball/y"]) + assert fields[0].name() == "ball/x" + assert fields[1].name() == "ball/y" + + with pytest.raises(IndexError): + _ = fields[2] + + def test_players(self) -> None: + cols = [ + "frame_id", + "teams/home/players/1/x", + "teams/home/players/2/x", + "teams/away/players/10/x", + "ball/x", + ] + fields = tdc.Fields(cols) + + players = fields.players() + assert len(players) == 3 + assert all(isinstance(p, tdc.Player) for p in players) + assert sorted([p.player_id for p in players]) == ["1", "10", "2"] + + home_players = fields.players(team="home") + assert len(home_players) == 2 + assert sorted([p.player_id for p in home_players]) == ["1", "2"] + assert all(p.team_id == "home" for p in home_players) + + away_players = fields.players(team="away") + assert len(away_players) == 1 + assert away_players[0].player_id == "10" + assert away_players[0].team_id == "away" + + assert fields.players(team="other") == [] + + def test_teams(self) -> None: + cols = [ + "teams/home/players/1/x", + "teams/away/players/10/x", + "teams/home/id", + "teams/away/formation", + ] + fields = tdc.Fields(cols) + + teams = fields.teams() + assert len(teams) == 2 + assert all(isinstance(t, tdc.Team) for t in teams) + assert sorted([t.team_id for t in teams]) == ["away", "home"] + + def test_referees(self) -> None: + cols = [ + "referees/ref_1/x", + "referees/ref_2/y", + "ball/x", + ] + fields = tdc.Fields(cols) + + referees = fields.referees() + assert len(referees) == 2 + assert all(isinstance(r, tdc.Referee) for r in referees) + assert sorted([r.referee_id for r in referees]) == ["ref_1", "ref_2"] + + def test_validate_success(self) -> None: + cols = [ + "frame_id", + "timestamp", + "period", + "match/id", + "ball/x", + "ball/y", + "ball/z", + "teams/home/id", + "teams/away/id", + "teams/home/players/1/x", + "teams/home/players/1/y", + "teams/away/players/10/x", + "teams/away/players/10/y", + ] + fields = tdc.Fields(cols) + # Should not raise + fields.validate() + assert fields.validate_quiet() is True + + def test_validate_missing_required(self) -> None: + # Missing ball/z and match/id + cols = [ + "frame_id", + "timestamp", + "period", + "ball/x", + "ball/y", + "teams/home/id", + "teams/away/id", + "teams/home/players/1/x", + "teams/home/players/1/y", + "teams/away/players/10/x", + "teams/away/players/10/y", + ] + fields = tdc.Fields(cols) + + with pytest.raises(tdc.exceptions.SchemaError) as excinfo: + fields.validate() + + assert "Missing ball/z" in str(excinfo.value) + assert "Missing match/id" in str(excinfo.value) + assert fields.validate_quiet() is False + + def test_validate_no_players(self) -> None: + # Teams exist but no players + cols = [ + "frame_id", + "timestamp", + "period", + "match/id", + "ball/x", + "ball/y", + "ball/z", + "teams/home/id", + "teams/away/id", + ] + fields = tdc.Fields(cols) + + with pytest.raises(tdc.exceptions.SchemaError, match="teams/home/players"): + fields.validate() + + def test_validate_player_missing_coord(self) -> None: + # Away player has x but no y + cols = [ + "frame_id", + "timestamp", + "period", + "match/id", + "ball/x", + "ball/y", + "ball/z", + "teams/home/id", + "teams/away/id", + "teams/home/players/1/x", + "teams/home/players/1/y", + "teams/away/players/10/x", + ] + fields = tdc.Fields(cols) + + with pytest.raises(tdc.exceptions.SchemaError, match="teams/away/players"): + fields.validate() + + def test_unsupported_field(self) -> None: + cols = [ + "frame_id", + "timestamp", + "period", + "match/id", + "ball/x", + "ball/y", + "ball/z", + "teams/home/id", + "teams/away/id", + "teams/home/players/1/x", + "teams/home/players/1/y", + "teams/away/players/10/x", + "teams/away/players/10/y", + "teams/home/xGoal", # Unsupported entity attribute + "dummy", # Unsupported frame attribute + ] + fields = tdc.Fields(cols) + + with pytest.warns(tdc.exceptions.SchemaWarning) as record: + fields.validate() + + msg = str(record[0].message) + assert "teams/home/xGoal" in msg + assert "dummy" in msg + assert fields.validate_quiet() is True diff --git a/python/tests/test_select.py b/python/tests/test_select.py new file mode 100644 index 0000000..3d8315a --- /dev/null +++ b/python/tests/test_select.py @@ -0,0 +1,78 @@ +import tracking_data_container as tdc + + +class TestSelection: + def test_container_select_string(self, container) -> None: + subset = container.select("ball/x") + assert subset.schema() == tdc.Fields(["ball/x"]) + + def test_container_select_list(self, container) -> None: + subset = container.select(["ball/x", "ball/y"]) + assert subset.schema() == tdc.Fields(["ball/x", "ball/y"]) + + def test_container_select_datafield(self, container) -> None: + field = tdc.DataField("ball/x") + subset = container.select(field) + assert subset.schema() == tdc.Fields(["ball/x"]) + + def test_container_select_selector_ball(self, container) -> None: + subset = container.select(container.selector.ball()) + assert subset.schema() == tdc.Fields(["ball/x", "ball/y", "ball/z"]) + + def test_container_select_selector_players(self, container) -> None: + # Add some player data to test player selection + data = { + "ball/x": [1.0], + "teams/home/players/1/x": [2.0], + "teams/home/players/1/y": [3.0], + "teams/away/players/10/x": [4.0], + } + c = tdc.TrackingDataContainer(data) + + subset = c.select(c.selector.players(team_id="home")) + assert subset.schema() == tdc.Fields(["teams/home/players/1/x", "teams/home/players/1/y"]) + + subset = c.select(c.selector.players().attributes(["x"])) + assert subset.schema() == tdc.Fields(["teams/away/players/10/x", "teams/home/players/1/x"]) + + def test_container_select_wildcard(self) -> None: + data = { + "ball/x": [1.0], + "ball/y": [1.1], + "teams/home/players/1/x": [2.0], + "teams/home/players/1/y": [3.0], + "teams/home/players/2/x": [4.0], + "teams/away/players/10/x": [5.0], + "teams/away/players/10/y": [5.1], + } + c = tdc.TrackingDataContainer(data) + + # 1. Select all x coordinates of the home team using ** + subset = c.select("teams/home/**/x") + assert sorted([f.name() for f in subset.schema()]) == sorted( + ["teams/home/players/1/x", "teams/home/players/2/x"] + ) + + # 2. Select x and y coordinates of the home team using list of ** + subset = c.select(["teams/home/**/x", "teams/home/**/y"]) + assert sorted([f.name() for f in subset.schema()]) == sorted( + ["teams/home/players/1/x", "teams/home/players/2/x", "teams/home/players/1/y"] + ) + + # 3. Select all x coordinates (players and ball) using leading ** + subset = c.select("**/x") + assert sorted([f.name() for f in subset.schema()]) == sorted( + [ + "ball/x", + "teams/home/players/1/x", + "teams/home/players/2/x", + "teams/away/players/10/x", + ] + ) + + # 4. Select x coordinates of home players using combined wildcards + # Using ** to skip segments and * for a single segment + subset = c.select("teams/home/**/players/*/x") + assert sorted([f.name() for f in subset.schema()]) == sorted( + ["teams/home/players/1/x", "teams/home/players/2/x"] + ) diff --git a/python/tests/test_transform.py b/python/tests/test_transform.py new file mode 100644 index 0000000..718900f --- /dev/null +++ b/python/tests/test_transform.py @@ -0,0 +1,179 @@ +import pytest +import tracking_data_container as tdc + + +class TestTransformations: + def test_transform_coordinates(self) -> None: + data = { + "ball/x": [0.0, 50.0, 100.0], + "ball/y": [0.0, 50.0, 100.0], + } + + # Define source system (Normalized 0-100) + source_sys = tdc.CoordinateSystem( + x_range=(0.0, 100.0), + y_range=(0.0, 100.0), + origin="bottom_left", + x_direction="right", + y_direction="up", + ) + + container = tdc.TrackingDataContainer(data).with_coordinate_system(source_sys) + + # Define target system (Metric -50 to 50) + target_sys = tdc.CoordinateSystem( + x_range=(-50.0, 50.0), + y_range=(-50.0, 50.0), + origin="center", + x_direction="right", + y_direction="up", + ) + + transformed = container.transform_coordinates(target_sys) + + df = transformed.to_polars() + # 0.0 -> -50.0 + # 50.0 -> 0.0 + # 100.0 -> 50.0 + res = df["ball/x"].to_list() + assert res[0] == pytest.approx(-50.0) + assert res[1] == pytest.approx(0.0) + assert res[2] == pytest.approx(50.0) + assert transformed.coordinate_system.origin == "center" + + def test_transform_units(self) -> None: + data = {"ball/x": [1.0], "ball/y": [1.0]} # 1 meter + + source_sys = tdc.CoordinateSystem(length_unit="meters", x_range=(0, 100), y_range=(0, 100)) + container = tdc.TrackingDataContainer(data).with_coordinate_system(source_sys) + + target_sys = tdc.CoordinateSystem(length_unit="yards", x_range=(0, 100), y_range=(0, 100)) + transformed = container.transform_coordinates(target_sys) + + # 1 meter = 1.09361 yards + df = transformed.to_polars() + assert df["ball/x"][0] == pytest.approx(1.09361, rel=1e-4) + + def test_transform_missing_units_raises(self) -> None: + data = {"ball/x": [1.0]} + container = tdc.TrackingDataContainer(data) # No metadata + + target_sys = tdc.CoordinateSystem(length_unit="yards") + + with pytest.raises(tdc.exceptions.InvalidParametersError, match="No coordinate system"): + container.transform_coordinates(target_sys) + + def test_transform_undefined_ranges_raises(self) -> None: + # Define system with origin only, no units or ranges + sys = tdc.CoordinateSystem(origin="center") + container = tdc.TrackingDataContainer({"ball/x": [1.0]}).with_coordinate_system(sys) + + target_sys = tdc.CoordinateSystem(length_unit="yards") + + with pytest.raises(tdc.exceptions.InvalidParametersError, match="Source X range undefined"): + container.transform_coordinates(target_sys) + + def test_transform_orientation(self) -> None: + data = { + "ball/x": [10.0, 10.0], + "period": ["first_half", "first_half"], + "ball_poss_team_id": ["home", "away"], + } + + # Source is static, we want to normalize it + container = tdc.TrackingDataContainer(data, orientation="static_home_away") + + # In "ball_owning_team" mode, if "away" has possession, we flip + transformed = container.transform_orientation("ball_owning_team") + + df = transformed.to_polars() + # Frame 1: home possession -> no flip -> 10.0 + # Frame 2: away possession -> flip -> -10.0 + assert df["ball/x"].to_list() == [10.0, -10.0] + + def test_transform_orientation_home_away(self) -> None: + data = { + "ball/x": [10.0, 10.0], + "period": ["first_half", "second_half"], + } + container = tdc.TrackingDataContainer(data, orientation="static_home_away") + + # HomeAway: second_half flips + transformed = container.transform_orientation("home_away") + assert transformed.to_polars()["ball/x"].to_list() == [10.0, -10.0] + + def test_transform_orientation_away_home(self) -> None: + data = { + "ball/x": [10.0, 10.0], + "period": ["first_half", "second_half"], + } + container = tdc.TrackingDataContainer(data, orientation="static_home_away") + + # AwayHome: first_half flips + transformed = container.transform_orientation("away_home") + assert transformed.to_polars()["ball/x"].to_list() == [-10.0, 10.0] + + def test_transform_orientation_identity(self) -> None: + data = { + "ball/x": [10.0, 10.0], + "period": ["first_half", "second_half"], + } + container = tdc.TrackingDataContainer(data).with_orientation(tdc.OrientationMode.AwayHome) + + # Transforming to the same mode should be a no-op + transformed = container.transform_orientation(tdc.OrientationMode.AwayHome) + assert transformed.orientation_mode == tdc.OrientationMode.AwayHome + assert transformed.to_polars()["ball/x"].to_list() == [10.0, 10.0] + + def test_transform_orientation_static_to_dynamic(self) -> None: + data = { + "ball/x": [10.0, 10.0], + "period": ["first_half", "second_half"], + } + # Data is already in AwayHome orientation but marked as StaticHomeAway + container = tdc.TrackingDataContainer(data).with_orientation( + tdc.OrientationMode.StaticHomeAway + ) + + # Transform to AwayHome: + # should_flip(StaticHomeAway) is always False + # should_flip(AwayHome) is True for first_half, False for second_half + # Result: first_half flips (False != True), second_half stays (False != False) + transformed = container.transform_orientation(tdc.OrientationMode.AwayHome) + assert transformed.to_polars()["ball/x"].to_list() == [-10.0, 10.0] + + def test_transform_orientation_dynamic_to_static(self) -> None: + data = { + "ball/x": [10.0, 10.0], + "period": ["first_half", "second_half"], + } + # Data is in AwayHome orientation + container = tdc.TrackingDataContainer(data).with_orientation(tdc.OrientationMode.AwayHome) + + # Transform to StaticHomeAway (which never flips) + # first_half: should_flip(AwayHome)=True, should_flip(StaticHomeAway)=False -> Flip + # second_half: should_flip(AwayHome)=False, should_flip(StaticHomeAway)=False -> No Flip + transformed = container.transform_orientation(tdc.OrientationMode.StaticHomeAway) + assert transformed.to_polars()["ball/x"].to_list() == [-10.0, 10.0] + + def test_transform_orientation_with_coordinate_system(self) -> None: + data = { + "ball/x": [10.0], + "period": ["first_half"], + } + raw_cs = tdc.CoordinateSystem( + x_range=(0, 100), + y_range=(0, 100), + origin=tdc.Origin.TopLeft, + ) + container = ( + tdc.TrackingDataContainer(data) + .with_coordinate_system(raw_cs) + .with_orientation(tdc.OrientationMode.AwayHome) + ) + + # AwayHome first_half flips. Transforming to StaticHomeAway (no flip) + # should trigger a flip from the AwayHome state. + # Flip across center of (0, 100) means 10.0 -> 90.0 + transformed = container.transform_orientation(tdc.OrientationMode.StaticHomeAway) + assert transformed.to_polars()["ball/x"][0] == pytest.approx(90.0) diff --git a/python/tracking_data_container/__init__.py b/python/tracking_data_container/__init__.py new file mode 100644 index 0000000..8a59eb1 --- /dev/null +++ b/python/tracking_data_container/__init__.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from . import exceptions +from ._tracking_data_container import ( + Attr, + Direction, + Expr, + Frame, + LengthUnit, + OrientationMode, + Origin, + __version__, + col, + lit, + ops, +) +from .builder import TrackingDataBuilder +from .container import LazyTrackingDataContainer, TrackingDataContainer +from .coordinate_system import CoordinateSystem +from .io import load_sportec, load_statsperform, scan_parquet +from .schema import ( + Ball, + DataField, + EntityDataField, + Fields, + FrameDataField, + Player, + Referee, + Team, + TrackedEntity, +) +from .selector import ColumnSelector, Selection + +__all__ = ( + "__version__", + "TrackingDataContainer", + "LazyTrackingDataContainer", + "TrackingDataBuilder", + "CoordinateSystem", + "Origin", + "Direction", + "LengthUnit", + "OrientationMode", + "ColumnSelector", + "Selection", + "scan_parquet", + "load_sportec", + "load_statsperform", + "DataField", + "TrackedEntity", + "Ball", + "Player", + "Team", + "Referee", + "FrameDataField", + "EntityDataField", + "Expr", + "Attr", + "Fields", + "Frame", + "col", + "lit", + "ops", + "exceptions", +) diff --git a/python/tracking_data_container/_tracking_data_container.pyi b/python/tracking_data_container/_tracking_data_container.pyi new file mode 100644 index 0000000..9efdad4 --- /dev/null +++ b/python/tracking_data_container/_tracking_data_container.pyi @@ -0,0 +1,286 @@ +from __future__ import annotations + +from datetime import datetime +from typing import TYPE_CHECKING, Any, Callable + +if TYPE_CHECKING: + import pyarrow as pa + +__version__: str + +class Attr: + """Standardized attribute names for tracking data.""" + + FrameId: Attr + Timestamp: Attr + Period: Attr + BallStatus: Attr + BallPossTeamId: Attr + BallPossStatus: Attr + X: Attr + Y: Attr + Z: Attr + Vel: Attr + Acc: Attr + Dist: Attr + @property + def value(self) -> str: ... + +class _TrackedEntity: + """ + Internal representation of a tracked entity. + """ + def __init__( + self, + team_id: str | None = None, + player_id: str | None = None, + referee_id: str | None = None, + ball: bool | None = None, + ) -> None: ... + def team_id(self) -> str | None: ... + def player_id(self) -> str | None: ... + def is_ball(self) -> bool: ... + def is_player(self) -> bool: ... + def is_team(self) -> bool: ... + def is_referee(self) -> bool: ... + +class _DataField: + """ + Internal representation of a data column. + """ + def __init__( + self, + path: str | None = None, + *, + attribute: Attr | str | None = None, + entity: _TrackedEntity | None = None, + ) -> None: ... + @staticmethod + def from_name(name: str) -> _DataField: ... + def team_id(self) -> str | None: ... + def player_id(self) -> str | None: ... + def is_frame(self) -> bool: ... + def is_entity(self) -> bool: ... + def entity(self) -> _TrackedEntity: ... + def attribute(self) -> str: ... + def name(self) -> str: ... + +class _Fields: + """Collection of DataFields representing a schema.""" + def __init__(self, columns: list[str]) -> None: ... + def __getitem__(self, index: int) -> _DataField: ... + def __len__(self) -> int: ... + def __iter__(self) -> _Fields: ... + def __next__(self) -> _DataField: ... + def player_entities(self, team: str | None = None) -> list[_TrackedEntity]: ... + def team_entities(self) -> list[_TrackedEntity]: ... + def referee_entities(self) -> list[_TrackedEntity]: ... + def validate(self) -> None: ... + def validate_quiet(self) -> bool: ... + +class Origin: + """Possible origin locations for a coordinate system.""" + + Center: Origin + TopLeft: Origin + BottomLeft: Origin + +class Direction: + """Possible axis directions for a coordinate system.""" + + Right: Direction + Left: Direction + Up: Direction + Down: Direction + +class LengthUnit: + """Standardized units of length.""" + + Meters: LengthUnit + Centimeters: LengthUnit + Millimeters: LengthUnit + Yards: LengthUnit + Inches: LengthUnit + Feet: LengthUnit + Percent: LengthUnit + +class OrientationMode: + """Normalization modes for playing direction.""" + + BallOwningTeam: OrientationMode + HomeAway: OrientationMode + AwayHome: OrientationMode + StaticHomeAway: OrientationMode + StaticAwayHome: OrientationMode + +class _CoordinateSystem: + """Internal representation of a coordinate system.""" + def __init__( + self, + x_range: tuple[float, float] | None = None, + y_range: tuple[float, float] | None = None, + origin: Origin | str = "center", + x_direction: Direction | str = "right", + y_direction: Direction | str = "up", + length_unit: LengthUnit | str | None = None, + ) -> None: ... + @property + def x_range(self) -> tuple[float, float] | None: ... + @property + def y_range(self) -> tuple[float, float] | None: ... + @property + def origin(self) -> str: ... + @property + def x_direction(self) -> str: ... + @property + def y_direction(self) -> str: ... + @property + def length_unit(self) -> str | None: ... + +class Selection: + """A selection of columns based on metadata.""" + + _inner: _Selection + def attributes(self, names: list[str]) -> Selection: ... + @property + def expr(self) -> str: ... + +class _Selection: + """Internal selection.""" + @property + def expr(self) -> str: ... + +class ColumnSelector: + """A selector for building column expressions based on metadata.""" + def ball(self) -> Selection: ... + def players(self, team_id: str | None = None) -> Selection: ... + def team(self, team_id: str) -> Selection: ... + +class Frame: + """A single frame of tracking data.""" + def __getitem__(self, key: str | _DataField) -> Any: ... + def to_dict(self) -> dict[_DataField, Any]: ... + +class _TrackingDataContainer: + """Eager tracking data container (internal).""" + def __init__( + self, + data: dict[str, list[Any]], + coordinate_system: _CoordinateSystem | None = None, + orientation: str | None = None, + ) -> None: ... + @classmethod + def _from_capsule(cls, capsule: Any) -> _TrackingDataContainer: ... + def lazy(self) -> _LazyTrackingDataContainer: ... + def select( + self, items: str | list[str | _DataField | _Selection] | _DataField | _Selection + ) -> _TrackingDataContainer: ... + def add_column( + self, name: str, value: Any, _schema_capsule: Any = None + ) -> _TrackingDataContainer: ... + def add_columns( + self, names: list[str], values: list[Any], _schema_capsules: list[Any] | None = None + ) -> _TrackingDataContainer: ... + def with_coordinate_system(self, system: _CoordinateSystem) -> _TrackingDataContainer: ... + def transform_coordinates(self, to_system: _CoordinateSystem) -> _TrackingDataContainer: ... + def transform_orientation(self, mode: OrientationMode | str) -> _TrackingDataContainer: ... + def with_orientation(self, mode: OrientationMode | str) -> _TrackingDataContainer: ... + @property + def schema(self) -> _Fields: ... + @property + def coordinate_system(self) -> _CoordinateSystem | None: ... + @property + def orientation_mode(self) -> OrientationMode | None: ... + @property + def selector(self) -> ColumnSelector: ... + def num_rows(self) -> int: ... + def validate(self) -> None: ... + def save_parquet(self, path: str) -> None: ... + @staticmethod + def load_parquet(path: str) -> _TrackingDataContainer: ... + def materialize_frame(self, frame: int | datetime | Any) -> Frame: ... + def to_arrow(self) -> pa.RecordBatch: ... + def __arrow_c_schema__(self) -> object: ... + def __arrow_c_array__(self, requested_schema: object = None) -> tuple[object, object]: ... + +class _LazyTrackingDataContainer: + """Lazy tracking data container (internal).""" + def collect(self) -> _TrackingDataContainer: ... + def select( + self, items: str | list[str | _DataField | _Selection] | _DataField | _Selection + ) -> _LazyTrackingDataContainer: ... + def filter(self, predicate: Expr) -> _LazyTrackingDataContainer: ... + def transform_coordinates(self, to_system: _CoordinateSystem) -> _LazyTrackingDataContainer: ... + def transform_orientation(self, mode: OrientationMode | str) -> _LazyTrackingDataContainer: ... + def add_column( + self, name: str, value: Any, _schema_capsule: Any = None + ) -> _LazyTrackingDataContainer: ... + def add_columns( + self, names: list[str], values: list[Any], _schema_capsules: list[Any] | None = None + ) -> _LazyTrackingDataContainer: ... + def add_column_udf( + self, name: str, func: Callable, on: list[str] + ) -> _LazyTrackingDataContainer: ... + @property + def schema(self) -> _Fields: ... + @property + def coordinate_system(self) -> _CoordinateSystem | None: ... + @property + def orientation_mode(self) -> OrientationMode | None: ... + @property + def selector(self) -> ColumnSelector: ... + def __arrow_c_schema__(self) -> object: ... + def __arrow_c_stream__(self, requested_schema: object = None) -> object: ... + +class _TrackingDataBuilder: + """Internal tracking data builder.""" + def __init__(self) -> None: ... + def insert_int(self, frame_id: int, col: str | _DataField, val: int) -> None: ... + def insert_float(self, frame_id: int, col: str | _DataField, val: float) -> None: ... + def insert_string(self, frame_id: int, col: str | _DataField, val: str) -> None: ... + def insert_bool(self, frame_id: int, col: str | _DataField, val: bool) -> None: ... + def merge(self, other: _TrackingDataBuilder) -> None: ... + def build(self) -> _TrackingDataContainer: ... + +class Expr: + """A logical expression for selecting or transforming data.""" + def __add__(self, other: Expr | float | int) -> Expr: ... + def __sub__(self, other: Expr | float | int) -> Expr: ... + def __mul__(self, other: Expr | float | int) -> Expr: ... + def __truediv__(self, other: Expr | float | int) -> Expr: ... + def eq(self, other: Expr | Any) -> Expr: ... + def ne(self, other: Expr | Any) -> Expr: ... + def gt(self, other: Expr | Any) -> Expr: ... + def ge(self, other: Expr | Any) -> Expr: ... + def lt(self, other: Expr | Any) -> Expr: ... + def le(self, other: Expr | Any) -> Expr: ... + def diff(self) -> Expr: ... + def pow(self, exponent: float) -> Expr: ... + def sqrt(self) -> Expr: ... + def replace_columns(self, mapping: dict[str, str]) -> Expr: ... + +def col(name: str) -> Expr: ... +def lit(value: Any) -> Expr: ... +def load_sportec(path: str, meta_path: str | None = None) -> _TrackingDataContainer: ... +def load_statsperform(path: str) -> _TrackingDataContainer: ... +def scan_parquet(path: str) -> _LazyTrackingDataContainer: ... + +class _Ops: + @staticmethod + def compute_speed(x_array: Any, y_array: Any, dt: float = 1.0) -> Any: ... + @staticmethod + def compute_acceleration(speed_array: Any, dt: float = 1.0) -> Any: ... + @staticmethod + def compute_jerk(acc_array: Any, dt: float = 1.0) -> Any: ... + @staticmethod + def compute_angular_velocity(vx_array: Any, vy_array: Any, dt: float = 1.0) -> Any: ... + @staticmethod + def butterworth_filter(array: Any, cutoff: float, fs: float, order: int = 2) -> Any: ... + +ops: _Ops + +# Exceptions +class TrackingDataContainerError(Exception): ... +class InvalidParametersError(TrackingDataContainerError): ... +class SchemaError(TrackingDataContainerError): ... +class SchemaWarning(UserWarning): ... diff --git a/python/tracking_data_container/builder.py b/python/tracking_data_container/builder.py new file mode 100644 index 0000000..5e3f43d --- /dev/null +++ b/python/tracking_data_container/builder.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +from ._tracking_data_container import ( + _DataField, + _TrackingDataBuilder, +) +from .container import TrackingDataContainer +from .schema import DataField + + +class TrackingDataBuilder: + """ + A builder for efficiently constructing a TrackingDataContainer. + + The builder allows incremental insertion of data points across multiple + frames and columns, optimized for memory and performance. + + Examples + -------- + The builder is designed to support parallel data ingestion by allowing + multiple builders to be populated independently and then merged. + + >>> from concurrent.futures import ThreadPoolExecutor + >>> from tracking_data_container import TrackingDataBuilder + >>> + >>> def process_chunk(chunk_data): + ... builder = TrackingDataBuilder() + ... for frame_id, x, y in chunk_data: + ... builder.insert(frame_id, "ball/x", x) + ... builder.insert(frame_id, "ball/y", y) + ... return builder + >>> + >>> chunks = [ + ... [(0, 10.0, 20.0), (1, 11.0, 21.0)], + ... [(2, 12.0, 22.0), (3, 13.0, 23.0)] + ... ] + >>> + >>> with ThreadPoolExecutor() as executor: + ... builders = list(executor.map(process_chunk, chunks)) + >>> + >>> main_builder = TrackingDataBuilder() + >>> for b in builders: + ... main_builder.merge(b) + >>> + >>> container = main_builder.build() + """ + + _builder: _TrackingDataBuilder + + def __init__(self) -> None: + self._builder = _TrackingDataBuilder() + + def insert(self, frame_id: int, col: str | DataField, val: int | float | str | bool) -> None: + """ + Insert a value into the builder. + + Parameters + ---------- + frame_id : int + The frame index. + col : str or DataField + The column name or data field. + val : int, float, str, or bool + The value to insert. + """ + processed_col: str | _DataField + if isinstance(col, DataField): + processed_col = col._field + else: + processed_col = col + + if isinstance(val, bool): + self._builder.insert_bool(frame_id, processed_col, val) + elif isinstance(val, int): + self._builder.insert_int(frame_id, processed_col, val) + elif isinstance(val, float): + self._builder.insert_float(frame_id, processed_col, val) + elif isinstance(val, str): + self._builder.insert_string(frame_id, processed_col, val) + else: + raise TypeError(f"Unsupported value type: {type(val)}") + + def merge(self, other: TrackingDataBuilder) -> None: + """Merge another builder into this one.""" + self._builder.merge(other._builder) + + def build(self) -> TrackingDataContainer: + """Build the final TrackingDataContainer.""" + new_inner = self._builder.build() + return TrackingDataContainer(new_inner) diff --git a/python/tracking_data_container/container.py b/python/tracking_data_container/container.py new file mode 100644 index 0000000..3311eda --- /dev/null +++ b/python/tracking_data_container/container.py @@ -0,0 +1,1079 @@ +from __future__ import annotations + +from datetime import datetime, timedelta +from typing import TYPE_CHECKING, Any, Callable, cast + +import numpy as np +import polars as pl +import pyarrow as pa + +from ._tracking_data_container import ( + Expr as _Expr, +) +from ._tracking_data_container import ( + Frame, + OrientationMode, + _DataField, + _LazyTrackingDataContainer, + _TrackingDataContainer, +) +from ._tracking_data_container import ( + Selection as _Selection, +) +from .coordinate_system import CoordinateSystem +from .schema import Ball, DataField, Fields +from .selector import ColumnSelector, Selection + +if TYPE_CHECKING: + from pathlib import Path + + import pandas as pd + import polars as pl + import pyarrow as pa + + +class TrackingDataContainer: + """ + A high-performance, metadata-aware container for sports tracking data. + + TrackingDataContainer acts as the primary eager interface for interacting + with tracking data, providing tools for selection, transformation, and + interoperability with the Python data ecosystem. + + Parameters + ---------- + data : dict or _TrackingDataContainer + The underlying data, either as a dictionary of columns or an existing + internal container. + coordinate_system : CoordinateSystem, optional + Spatial metadata describing the coordinate frame. + orientation : str or OrientationMode, optional + The playing direction normalization mode. + + Examples + -------- + >>> import tracking_data_container as tdc + >>> data = {"ball/x": [0.0, 1.0], "ball/y": [0.0, 1.0]} + >>> container = tdc.TrackingDataContainer(data) + >>> container.num_rows() + 2 + >>> # Select specific columns + >>> ball_pos = container.select(["ball/x", "ball/y"]) + >>> # Convert to other formats + >>> df = container.to_polars() + """ + + _container: _TrackingDataContainer + + def __init__( + self, + data: dict[str, Any] | _TrackingDataContainer | pl.DataFrame, + coordinate_system: CoordinateSystem | None = None, + orientation: str | OrientationMode | None = None, + ) -> None: + if isinstance(data, _TrackingDataContainer): + self._container = data + else: + if isinstance(data, pl.DataFrame): + data = data.to_dict(as_series=False) + + # Deep normalization of the dictionary + normalized_data = {} + for k, v in data.items(): + if v is None or len(v) == 0: + normalized_data[k] = v + continue + + # Ensure all inputs are native python types for StructArray construction + if isinstance(v, (np.ndarray, pl.Series, pa.Array)): + # Special handling for datetime/timedelta to microseconds + if k == "timestamp": + if isinstance(v, np.ndarray): + if v.dtype.kind == "M": + normalized_data[k] = [ + int(dt.astype("datetime64[us]").view(np.int64)) for dt in v + ] + else: + normalized_data[k] = v.tolist() + elif isinstance(v, pl.Series): + normalized_data[k] = v.dt.timestamp("us").to_list() + else: + normalized_data[k] = v.to_pylist() + # Ensure coordinates are Float32 + elif k.endswith("/x") or k.endswith("/y") or k.endswith("/z"): + normalized_data[k] = [float(x) for x in v] + else: + normalized_data[k] = v.tolist() if hasattr(v, "tolist") else list(v) + elif isinstance(v, list) and len(v) > 0: + if isinstance(v[0], timedelta): + normalized_data[k] = [int(td.total_seconds() * 1_000_000) for td in v] + elif isinstance(v[0], datetime): + normalized_data[k] = [int(dt.timestamp() * 1_000_000) for dt in v] + else: + normalized_data[k] = v + else: + normalized_data[k] = v + + inner_cs = coordinate_system._inner if coordinate_system else None + orientation_str = str(orientation) if orientation else None + + self._container = _TrackingDataContainer(normalized_data, inner_cs, orientation_str) + + # --- Properties and Metadata --- + + def schema(self) -> Fields: + """Returns the schema of the container.""" + return Fields(self._container.schema) + + @property + def coordinate_system(self) -> CoordinateSystem | None: + """Returns the active coordinate system metadata.""" + if self._container.coordinate_system is None: + return None + return CoordinateSystem._from_inner(self._container.coordinate_system) + + @property + def orientation_mode(self) -> OrientationMode | None: + """Returns the active orientation normalization mode.""" + return self._container.orientation_mode + + @property + def selector(self) -> ColumnSelector: + """Returns the selector for building column expressions.""" + return ColumnSelector(self._container.selector) + + def num_rows(self) -> int: + """Returns the number of frames in the container.""" + return self._container.num_rows() + + def validate(self) -> None: + """ + Validates the container against the Common Data Format (CDF). + + Raises + ------ + SchemaError + If mandatory columns are missing or have incorrect types. + SchemaWarning + If non-standard columns are present. + """ + self._container.validate() + + def __len__(self) -> int: + return self.num_rows() + + def __getitem__(self, key: int) -> Frame: + """Access a specific frame by index.""" + return self.materialize_frame(key) + + # --- Transformations --- + + def with_coordinate_system(self, system: CoordinateSystem) -> TrackingDataContainer: + """ + Sets the coordinate system metadata without performing any transformations. + + Parameters + ---------- + system : CoordinateSystem + The new coordinate system. + + Returns + ------- + TrackingDataContainer + A new container with updated metadata. + """ + new_inner = self._container.with_coordinate_system(system._inner) + return TrackingDataContainer(new_inner) + + def transform_coordinates(self, to_system: CoordinateSystem) -> TrackingDataContainer: + """ + Transforms coordinates to a new spatial system. + + Parameters + ---------- + to_system : CoordinateSystem + The target coordinate system. + + Returns + ------- + TrackingDataContainer + A new container with transformed coordinates. + """ + new_inner = self._container.transform_coordinates(to_system._inner) + return TrackingDataContainer(new_inner) + + def with_orientation(self, mode: str | OrientationMode) -> TrackingDataContainer: + """ + Sets the orientation metadata without performing any transformations. + + Parameters + ---------- + mode : str or OrientationMode + The new orientation mode. + + Returns + ------- + TrackingDataContainer + A new container with updated metadata. + """ + if not isinstance(mode, str): + mode = str(mode) + new_inner = self._container.with_orientation(mode) + return TrackingDataContainer(new_inner) + + def transform_orientation(self, mode: str | OrientationMode) -> TrackingDataContainer: + """ + Normalizes coordinates based on playing direction. + + Parameters + ---------- + mode : str or OrientationMode + The orientation normalization mode. + + Returns + ------- + TrackingDataContainer + A new container with normalized coordinates. + """ + if not isinstance(mode, str): + mode = str(mode) + new_inner = self._container.transform_orientation(mode) + return TrackingDataContainer(new_inner) + + # --- Selections and Data Additions --- + + def select(self, items: str | list[str | DataField | Selection]) -> TrackingDataContainer: + """ + Select columns from the container. + + Parameters + ---------- + items : str or list of str or DataField or Selection + The columns to select. + + Returns + ------- + TrackingDataContainer + A new container with only the selected columns. + """ + processed_items: str | list[str | _DataField | _Selection] | _DataField | _Selection + if isinstance(items, list): + processed_items = [] + for i in items: + if isinstance(i, DataField): + processed_items.append(i._field) + elif isinstance(i, Selection): + processed_items.append(i._selection) + else: + processed_items.append(i) + elif isinstance(items, DataField): + processed_items = items._field + elif isinstance(items, Selection): + processed_items = items._selection + else: + processed_items = items + + new_inner = self._container.select(processed_items) # type: ignore + new_container = TrackingDataContainer.__new__(TrackingDataContainer) + new_container._container = new_inner + return new_container + + def add_column( + self, + name: str, + value: Any | Callable[[pl.DataFrame], Any], + on: str | list[str] | None = None, + ) -> TrackingDataContainer: + """ + Add a new column to the container. + + Parameters + ---------- + name : str + The name of the new column. + value : array-like or callable + The data for the new column. If a callable is provided, it will be + invoked with the columns specified in `on` as separate arguments. + on : str or list of str, optional + The columns to pass to the callable. + + Returns + ------- + TrackingDataContainer + A new container including the new column. + """ + import polars as pl + + if callable(value): + inputs = self.select(on) if on is not None else self # type: ignore + batch = inputs.to_arrow() + if on is not None: + # Pass individual columns as pyarrow arrays + on_list = [on] if isinstance(on, str) else list(on) + args = [batch.column(c) for c in on_list] + data = value(*args) + else: + # Pass the whole record batch + data = value(batch) + + if isinstance(data, pl.DataFrame) and data.width == 1: + data = data.to_series() + elif isinstance(value, _Expr): + # If it's an expression, pass it to the Rust implementation + new_inner = self._container.add_column(name, value) + new_container = TrackingDataContainer.__new__(TrackingDataContainer) + new_container._container = new_inner + return new_container + else: + data = value + + try: + array_capsule, schema_capsule = data.__arrow_c_array__() + except (AttributeError, TypeError): + import pyarrow as pa + + if isinstance(data, pl.Series): + data = pa.array(data.to_list()) + else: + data = pa.array(data) + array_capsule, schema_capsule = data.__arrow_c_array__() + + new_inner = self._container.add_column(name, array_capsule, schema_capsule) + new_container = TrackingDataContainer.__new__(TrackingDataContainer) + new_container._container = new_inner + return new_container + + def add_columns( + self, + names: list[str], + values: list[Any], + on: list[str | list[str] | None] | None = None, + ) -> TrackingDataContainer: + """ + Add multiple new columns to the container in a single operation. + + Parameters + ---------- + names : list of str + The names for the new columns. + values : list of array-like, Expr, or callable + The data or computations for the new columns. + on : list of str or list of list of str, optional + The columns to pass to each callable value. + + Returns + ------- + TrackingDataContainer + A new container including the new columns. + """ + import polars as pl + import pyarrow as pa + + processed_values = [] + schema_capsules: list[Any] = [] + + for i, (name, value) in enumerate(zip(names, values)): + val_on = on[i] if on is not None else None + if callable(value): + # Try to evaluate as expression first if on is provided + data = None + if val_on is not None: + try: + # Attempt to pass string names for Expr building pass + # If val_on is a single string, wrap it in a tuple + on_tuple: tuple[str, ...] = ( + (val_on,) if isinstance(val_on, str) else tuple(val_on) + ) + data = value(*on_tuple) + if not isinstance(data, _Expr): + # Not an expression, fallback to eager series + data = None + except Exception: + data = None + + if data is None: + # Eager evaluation path + inputs = self.select(val_on) if val_on is not None else self # type: ignore + batch = inputs.to_arrow() + if val_on is not None: + # Pass individual columns as pyarrow arrays + on_list = [val_on] if isinstance(val_on, str) else list(val_on) + args = [batch.column(c) for c in on_list] + data = value(*args) + else: + # Pass the whole record batch + data = value(batch) + + if isinstance(data, pl.DataFrame) and data.width == 1: + data = data.to_series() + else: + data = value + if isinstance(data, _Expr): + processed_values.append(data) + schema_capsules.append(None) + else: + # Array-like + try: + array_capsule, schema_capsule = data.__arrow_c_array__() # type: ignore + except (AttributeError, TypeError): + if isinstance(data, pl.Series): + data = pa.array(data.to_list()) + else: + data = pa.array(data) + array_capsule, schema_capsule = data.__arrow_c_array__() + processed_values.append(array_capsule) + schema_capsules.append(schema_capsule) + + new_inner = self._container.add_columns(names, processed_values, schema_capsules) + new_container = TrackingDataContainer.__new__(TrackingDataContainer) + new_container._container = new_inner + return new_container + + def add_column_per_player( + self, name: str, value: Any, on: str | list[str] | None = None + ) -> TrackingDataContainer: + """ + Add a new column for every player in the container. + + Parameters + ---------- + name : str + The attribute name for the new columns. + value : array-like, Expr, or callable + The data or computation for the new columns. + on : str or list of str, optional + The player-specific attributes to use for the computation. + + Returns + ------- + TrackingDataContainer + A new container with added columns for each player. + """ + names = [] + values = [] + ons = [] + + for player in self.schema().players(): + col_name = DataField(entity=player, attribute=name).name() + names.append(col_name) + + player_on: str | list[str] | None = None + if on is not None: + if isinstance(on, str): + player_on = DataField(entity=player, attribute=on).name() + elif isinstance(on, list): + player_on = [DataField(entity=player, attribute=attr).name() for attr in on] + + ons.append(player_on) + + if isinstance(value, _Expr) and on is not None: + # Optimized path for shared expressions: substitute placeholders + on_list = [on] if isinstance(on, str) else on + mapping = { + attr: DataField(entity=player, attribute=attr).name() for attr in on_list + } + values.append(value.replace_columns(mapping)) + continue + + if callable(value) and on is not None: + on_resolved = player_on if isinstance(player_on, list) else [player_on] # type: ignore + # Check if it returns an Expr + try: + result = value(*on_resolved) # type: ignore + if isinstance(result, _Expr): + values.append(result) + continue + except Exception: + pass + + values.append(value) + + return self.add_columns(names, values, on=ons) # type: ignore + + def lazy(self) -> LazyTrackingDataContainer: + """ + Converts the container to a lazy version for deferred execution. + + Returns + ------- + LazyTrackingDataContainer + The lazy version of this container. + """ + new_inner = self._container.lazy() + new_container = LazyTrackingDataContainer.__new__(LazyTrackingDataContainer) + new_container._container = new_inner + return new_container + + # --- Materialization and Visualization --- + + def materialize_frame(self, frame: int | datetime | timedelta) -> Frame: + """ + Returns the values of all fields for a specific frame. + + Parameters + ---------- + frame : int, datetime, or timedelta + The frame index or time to materialize. + + Returns + ------- + Frame + The materialized frame data. + """ + if isinstance(frame, int): + index = frame + elif isinstance(frame, datetime): + # Resolve index from timestamp using integer comparison + target_ts = int(frame.timestamp() * 1_000_000) + df = self.select(["timestamp"]).to_polars() + try: + # Use numpy for the comparison to avoid Polars Datetime eq issues + ts_array = df["timestamp"].dt.timestamp("us").to_numpy() + index = int(np.where(ts_array == target_ts)[0][0]) + except (IndexError, KeyError): + raise ValueError(f"Timestamp {frame} not found in container") + elif isinstance(frame, timedelta): + # Resolve index from offset + target_us = int(frame.total_seconds() * 1_000_000) + df = self.select(["timestamp"]).to_polars() + try: + # Compare relative to first frame + ts_array = df["timestamp"].dt.timestamp("us").to_numpy() + if len(ts_array) > 0: + start_ts = ts_array[0] + index = int(np.where(ts_array == (start_ts + target_us))[0][0]) + else: + raise IndexError("Container is empty") + except (IndexError, KeyError): + # Fallback to frame_id if timestamp resolution fails + try: + df_id = self.select(["frame_id"]).to_polars() + # Assume 25fps if no metadata, or use frame_id starting at 0 + target_frame = int(frame.total_seconds() * 25) + index = int(np.where(df_id["frame_id"] == target_frame)[0][0]) + except (IndexError, KeyError): + raise ValueError(f"Timedelta {frame} could not be resolved to a frame index") + else: + raise TypeError(f"Unsupported frame type: {type(frame)}") + + return self._container.materialize_frame(index) + + def plot_frame( + self, + frame: int | datetime | timedelta, + ax: Any = None, + home_color: str = "red", + away_color: str = "blue", + ball_color: str = "white", + referee_color: str = "yellow", + player_size: float = 80, + ball_size: float = 50, + **kwargs: Any, + ) -> Any: + """ + Visualizes a frame using mplsoccer. + + Parameters + ---------- + frame : int, datetime, or timedelta + The frame index, timestamp, or timedelta to plot. + ax : matplotlib.axes.Axes, optional + An existing matplotlib Axes object to plot on. + home_color : str, default="red" + Color for the home team players. + away_color : str, default="blue" + Color for the away team players. + ball_color : str, default="white" + Color for the ball. + referee_color : str, default="yellow" + Color for the referees. + player_size : float, default=0.8 + Radius of the players in pitch units. + ball_size : float, default=0.5 + Radius of the ball in pitch units. + **kwargs + Additional arguments passed to the Pitch.scatter method. + + Returns + ------- + matplotlib.axes.Axes + The matplotlib Axes object. + + Raises + ------ + ImportError + If mplsoccer is not installed. + ValueError + If the coordinate system is not defined. + """ + try: + from mplsoccer import Pitch + except ImportError: + raise ImportError( + "Seems like you don't have `mplsoccer` installed. " + "Please install it using: pip install mplsoccer" + ) + + if self.coordinate_system is None: + raise ValueError( + "Coordinate system must be defined to plot. " + "Use container.with_coordinate_system() to set it." + ) + + dot_styles = { + "home": { + "facecolor": home_color, + "edgecolor": "white", + "linewidth": 0.8, + "s": player_size, + "zorder": 5, + }, + "away": { + "facecolor": away_color, + "edgecolor": "white", + "linewidth": 0.8, + "s": player_size, + "zorder": 5, + }, + "ball": { + "facecolor": ball_color, + "edgecolor": "black", + "s": ball_size, + "zorder": 6, + }, + "referee": { + "facecolor": referee_color, + "edgecolor": "black", + "linewidth": 0.8, + "s": player_size, + "zorder": 5, + }, + } + + dims = self.coordinate_system.to_mplsoccer() + pitch = Pitch(pitch_type=dims) + + if ax is None: + fig, ax = pitch.draw() + else: + pitch.draw(ax=ax) + + f = self.materialize_frame(frame) + schema = self.schema() + + # Plot players + for tracked_team in schema.teams(): + from .schema import Team + + if not isinstance(tracked_team, Team): + continue + team_id = tracked_team.team_id + if team_id is None: + continue + style_key = "home" if "home" in team_id.lower() else "away" + style = dot_styles[style_key].copy() + style.update(kwargs) + + players = schema.players(team=team_id) + x = [] + y = [] + for p in players: + px = f[DataField(entity=p, attribute="x")] # type: ignore + py = f[DataField(entity=p, attribute="y")] # type: ignore + if px is not None and py is not None: + x.append(px) + y.append(py) + + if x: + pitch.scatter(x, y, ax=ax, label=f"Team {team_id}", **style) + + # Plot ball + bx = f[DataField(entity=Ball(), attribute="x")] # type: ignore + by = f[DataField(entity=Ball(), attribute="y")] # type: ignore + if bx is not None and by is not None: + style = dot_styles["ball"].copy() + style.update(kwargs) + pitch.scatter(bx, by, ax=ax, label="Ball", **style) + + # Plot referees + rx = [] + ry = [] + for ref in schema.referees(): + px = f[DataField(entity=ref, attribute="x")] # type: ignore + py = f[DataField(entity=ref, attribute="y")] # type: ignore + if px is not None and py is not None: + rx.append(px) + ry.append(py) + + if rx: + style = dot_styles["referee"].copy() + style.update(kwargs) + pitch.scatter(rx, ry, ax=ax, label="Referees", **style) + + return ax + + # --- Interoperability and Export --- + + def to_arrow(self) -> pa.RecordBatch: + """Converts the container to a pyarrow `RecordBatch`.""" + import pyarrow as pa + + return pa.record_batch(self._container.to_arrow()) + + def to_pandas(self) -> pd.DataFrame: + """Converts the container to a pandas `DataFrame`.""" + return cast("pd.DataFrame", self.to_arrow().to_pandas()) + + def to_polars(self) -> pl.DataFrame: + """Converts the container to a polars `DataFrame`.""" + import polars as pl + + return cast(pl.DataFrame, pl.DataFrame(self)) + + def to_parquet(self, path: str | Path) -> None: + """Saves the container to a Parquet file.""" + self._container.save_parquet(str(path)) + + @classmethod + def from_parquet(cls, path: str | Path) -> TrackingDataContainer: + """Loads a container from a Parquet file.""" + new_inner = _TrackingDataContainer.load_parquet(str(path)) + new_container = TrackingDataContainer.__new__(TrackingDataContainer) + new_container._container = new_inner + return new_container + + # --- Internal and Special Methods --- + + def __arrow_c_schema__(self) -> object: + return self._container.__arrow_c_schema__() + + def __arrow_c_array__(self, requested_schema: object | None = None) -> object: + return self._container.__arrow_c_array__(requested_schema) + + def __repr__(self) -> str: + return repr(self._container) + + +class LazyTrackingDataContainer: + """ + A lazy version of the tracking data container for deferred execution. + + LazyTrackingDataContainer allows building complex query plans that are + optimized and executed only when explicitly requested via `collect`. + """ + + _container: _LazyTrackingDataContainer + + def __init__(self, container: _LazyTrackingDataContainer): + self._container = container + + # --- Properties and Metadata --- + + def schema(self) -> Fields: + """Returns the schema of the lazy container.""" + return Fields(self._container.schema) + + @property + def coordinate_system(self) -> CoordinateSystem | None: + """Returns the active coordinate system metadata.""" + if self._container.coordinate_system is None: + return None + return CoordinateSystem._from_inner(self._container.coordinate_system) + + @property + def orientation_mode(self) -> OrientationMode | None: + """Returns the active orientation normalization mode.""" + return self._container.orientation_mode + + @property + def selector(self) -> ColumnSelector: + """Returns the selector for building column expressions.""" + return ColumnSelector(self._container.selector) + + # --- Selections and Data Additions --- + + def select(self, items: str | list[str | DataField | Selection]) -> LazyTrackingDataContainer: + """ + Lazily select columns from the container. + + Parameters + ---------- + items : str or list of str or DataField or Selection + The columns to select. + + Returns + ------- + LazyTrackingDataContainer + A new lazy container with the selection added to the plan. + """ + processed_items: str | list[str | _DataField | _Selection] | _DataField | _Selection + if isinstance(items, list): + processed_items = [] + for i in items: + if isinstance(i, DataField): + processed_items.append(i._field) + elif isinstance(i, Selection): + processed_items.append(i._selection) + else: + processed_items.append(i) + elif isinstance(items, DataField): + processed_items = items._field + elif isinstance(items, Selection): + processed_items = items._selection + else: + processed_items = items + + new_inner = self._container.select(processed_items) # type: ignore + return LazyTrackingDataContainer(new_inner) + + def filter(self, predicate: _Expr) -> LazyTrackingDataContainer: + """ + Lazily filter rows from the container. + + Parameters + ---------- + predicate : _Expr + The expression defining the filter condition. + + Returns + ------- + LazyTrackingDataContainer + A new lazy container with the filter added to the plan. + """ + new_inner = self._container.filter(predicate) + return LazyTrackingDataContainer(new_inner) + + def transform_coordinates(self, to_system: CoordinateSystem) -> LazyTrackingDataContainer: + """ + Lazily transforms coordinates to a new spatial system. + + Parameters + ---------- + to_system : CoordinateSystem + The target coordinate system. + + Returns + ------- + LazyTrackingDataContainer + A new lazy container with transformed coordinates added to the plan. + """ + new_inner = self._container.transform_coordinates(to_system._inner) + return LazyTrackingDataContainer(new_inner) + + def transform_orientation(self, mode: str | OrientationMode) -> LazyTrackingDataContainer: + """ + Lazily normalizes coordinates based on playing direction. + + Parameters + ---------- + mode : str or OrientationMode + The orientation normalization mode. + + Returns + ------- + LazyTrackingDataContainer + A new lazy container with normalized coordinates added to the plan. + """ + if not isinstance(mode, str): + mode = str(mode) + new_inner = self._container.transform_orientation(mode) + return LazyTrackingDataContainer(new_inner) + + def add_column( + self, name: str, value: Any, on: str | list[str] | None = None + ) -> LazyTrackingDataContainer: + """ + Lazily add a new column to the container. + + Parameters + ---------- + name : str + The name of the new column. + value : Expr, array-like, or callable + The computation or data for the new column. + on : str or list of str, optional + The columns required for the computation (required for callables). + + Returns + ------- + LazyTrackingDataContainer + A new lazy container with the column addition added to the plan. + """ + import pyarrow as pa + + from ._tracking_data_container import Expr as _PyExpr + + if hasattr(value, "_expr") and isinstance(value._expr, _PyExpr): + new_inner = self._container.add_column(name, value._expr, None) + elif isinstance(value, _PyExpr): + new_inner = self._container.add_column(name, value, None) + elif callable(value): + if on is None: + raise ValueError( + "The 'on' parameter must be provided when using a callable in lazy mode" + ) + # Ensure on is a list of strings + on_list = [on] if isinstance(on, str) else list(on) + new_inner = self._container.add_column_udf(name, value, on_list) + else: + data = pa.array(value) + array_capsule, schema_capsule = data.__arrow_c_array__() + new_inner = self._container.add_column(name, array_capsule, schema_capsule) + + return LazyTrackingDataContainer(new_inner) + + def add_columns( + self, + names: list[str], + values: list[Any], + on: list[str | list[str] | None] | None = None, + ) -> LazyTrackingDataContainer: + """ + Lazily add multiple new columns to the container. + + Parameters + ---------- + names : list of str + The names for the new columns. + values : list of Expr, array-like, or callable + The data or computations for the new columns. + on : list of str or list of list of str, optional + The columns to pass to each callable value. + + Returns + ------- + LazyTrackingDataContainer + A new lazy container including the new columns. + """ + import pyarrow as pa + + from ._tracking_data_container import Expr as _PyExpr + + processed_values = [] + schema_capsules: list[Any] = [] + + for i, (name, value) in enumerate(zip(names, values)): + val_on = on[i] if on is not None else None + if callable(value): + # Eager evaluation for callables in lazy mode is not supported + # unless they return an Expr. + if val_on is None: + raise ValueError( + "The 'on' parameter must be provided when using a callable in lazy mode" + ) + # Try to evaluate as expression + on_tuple: tuple[str, ...] = (val_on,) if isinstance(val_on, str) else tuple(val_on) + data = value(*on_tuple) + if not isinstance(data, _PyExpr): + raise TypeError("Callables in lazy mode must return an Expr") + elif hasattr(value, "_expr") and isinstance(value._expr, _PyExpr): + data = value._expr + else: + data = value + + if isinstance(data, _PyExpr): + processed_values.append(data) + schema_capsules.append(None) + else: + # Array-like + array_data = pa.array(data) + array_capsule, schema_capsule = array_data.__arrow_c_array__() + processed_values.append(array_capsule) + schema_capsules.append(schema_capsule) + + new_inner = self._container.add_columns(names, processed_values, schema_capsules) + return LazyTrackingDataContainer(new_inner) + + def add_column_per_player( + self, name: str, value: Any, on: str | list[str] | None = None + ) -> LazyTrackingDataContainer: + """ + Lazily add a new column for every player in the container. + + Parameters + ---------- + name : str + The attribute name for the new columns. + value : Expr, array-like, or callable + The computation or data for the new columns. + on : str or list of str, optional + The player-specific attributes to use for the computation. + + Returns + ------- + LazyTrackingDataContainer + A new lazy container with the per-player columns added to the plan. + """ + names = [] + values = [] + ons = [] + + from ._tracking_data_container import Expr as _PyExpr + + for player in self.schema().players(): + col_name = DataField(entity=player, attribute=name).name() + names.append(col_name) + + player_on: str | list[str] | None = None + if on is not None: + if isinstance(on, str): + player_on = DataField(entity=player, attribute=on).name() + elif isinstance(on, list): + player_on = [DataField(entity=player, attribute=attr).name() for attr in on] + + ons.append(player_on) + + if ( + isinstance(value, _PyExpr) + or (hasattr(value, "_expr") and isinstance(value._expr, _PyExpr)) + ) and on is not None: + expr = value._expr if hasattr(value, "_expr") else value + on_list = [on] if isinstance(on, str) else on + mapping = { + attr: DataField(entity=player, attribute=attr).name() for attr in on_list + } + values.append(expr.replace_columns(mapping)) + continue + + if callable(value) and on is not None: + on_resolved = player_on if isinstance(player_on, list) else [player_on] # type: ignore + # Check if it returns an Expr + try: + result = value(*on_resolved) # type: ignore + if isinstance(result, _PyExpr) or ( + hasattr(result, "_expr") and isinstance(result._expr, _PyExpr) + ): + expr = result._expr if hasattr(result, "_expr") else result + values.append(expr) + continue + except Exception: + pass + + values.append(value) + + return self.add_columns(names, values, on=ons) + + # --- Execution --- + + def collect(self) -> TrackingDataContainer: + """ + Executes the lazy plan and returns an eager `TrackingDataContainer`. + + Returns + ------- + TrackingDataContainer + The resulting eager container. + """ + new_inner = self._container.collect() + return TrackingDataContainer(new_inner) + + # --- Internal and Special Methods --- + + def __arrow_c_schema__(self) -> object: + return self._container.__arrow_c_schema__() + + def __arrow_c_stream__(self, requested_schema: object | None = None) -> object: + return self._container.__arrow_c_stream__(requested_schema) + + def __repr__(self) -> str: + return repr(self._container) diff --git a/python/tracking_data_container/coordinate_system.py b/python/tracking_data_container/coordinate_system.py new file mode 100644 index 0000000..6ea6d3a --- /dev/null +++ b/python/tracking_data_container/coordinate_system.py @@ -0,0 +1,233 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ._tracking_data_container import ( + Direction, + LengthUnit, + Origin, + _CoordinateSystem, +) + +if TYPE_CHECKING: + from mplsoccer.soccer.dimensions import BaseSoccerDims + + +class CoordinateSystem: + """ + Defines a coordinate system for tracking data. + + A coordinate system combines spatial bounds (ranges), an origin point, axis + orientations, and measurement units to provide a complete context for entity + positions. + + Parameters + ---------- + x_range : tuple[float, float], optional + The valid range of X coordinates (e.g., `(-52.5, 52.5)`). + y_range : tuple[float, float], optional + The valid range of Y coordinates (e.g., `(-34.0, 34.0)`). + origin : Origin or str, default="center" + The location of the (0,0) point. Can be "center", "top_left", or "bottom_left". + x_direction : Direction or str, default="right" + The direction in which X values increase. Can be "right" or "left". + y_direction : Direction or str, default="up" + The direction in which Y values increase. Can be "up" or "down". + length_unit : LengthUnit or str, optional + The physical unit of length for the coordinates. Can be "meters", + "centimeters", "millimeters", "yards", "inches", "feet", or "percent". + + Examples + -------- + Defining a standard metric coordinate system: + + >>> from tracking_data_container import CoordinateSystem + >>> metric_sys = CoordinateSystem( + ... x_range=(-52.5, 52.5), + ... y_range=(-34.0, 34.0), + ... origin="center", + ... length_unit="meters" + ... ) + + Defining a normalized coordinate system (0 to 1): + + >>> norm_sys = CoordinateSystem( + ... x_range=(0.0, 1.0), + ... y_range=(0.0, 1.0), + ... origin="bottom_left", + ... length_unit="percent" + ... ) + """ + + _inner: _CoordinateSystem + + def __init__( + self, + x_range: tuple[float, float] | None = None, + y_range: tuple[float, float] | None = None, + origin: Origin | str = Origin.Center, + x_direction: Direction | str = Direction.Right, + y_direction: Direction | str = Direction.Up, + length_unit: LengthUnit | str | None = None, + ) -> None: + self._inner = _CoordinateSystem( + x_range=x_range, + y_range=y_range, + origin=origin, + x_direction=x_direction, + y_direction=y_direction, + length_unit=length_unit, + ) + + @staticmethod + def _from_inner(inner: _CoordinateSystem) -> CoordinateSystem: + """Internal helper to create a CoordinateSystem from the Rust representation.""" + instance = CoordinateSystem.__new__(CoordinateSystem) + instance._inner = inner + return instance + + @property + def x_range(self) -> tuple[float, float] | None: + """The valid range of X coordinates.""" + return self._inner.x_range + + @property + def y_range(self) -> tuple[float, float] | None: + """The valid range of Y coordinates.""" + return self._inner.y_range + + @property + def origin(self) -> str: + """The location of the (0,0) point.""" + return self._inner.origin + + @property + def x_direction(self) -> str: + """The direction in which X values increase.""" + return self._inner.x_direction + + @property + def y_direction(self) -> str: + """The direction in which Y values increase.""" + return self._inner.y_direction + + @property + def length_unit(self) -> str | None: + """The physical unit of length for the coordinates.""" + return self._inner.length_unit + + def __repr__(self) -> str: + return repr(self._inner) + + def to_mplsoccer(self) -> BaseSoccerDims: + """ + Convert the coordinate system to a mplsoccer BaseSoccerDims object. + + This allows using the coordinate system directly with mplsoccer's Pitch class. + + Returns + ------- + mplsoccer.soccer.dimensions.BaseSoccerDims + The dimensions object for mplsoccer. + + Examples + -------- + >>> from tracking_data_container import CoordinateSystem + >>> from mplsoccer import Pitch + >>> coordinate_system = CoordinateSystem(x_range=(-52.5, 52.5), y_range=(-34, 34)) + >>> pitch = Pitch(pitch_type=coordinate_system.to_mplsoccer()) + + Notes + ----- + This method is experimental and requires the `mplsoccer` package. + """ + try: + from mplsoccer.soccer.dimensions import BaseSoccerDims + except ImportError: + raise ImportError( + "Seems like you don't have `mplsoccer` installed. " + "Please install it using: pip install mplsoccer" + ) + + if self.x_range is None or self.y_range is None: + raise ValueError("The x and y dimensions of the pitch must be fully defined.") + + x_min, x_max = self.x_range + y_min, y_max = self.y_range + + width = x_max - x_min + length = y_max - y_min + + # Estimate common football pitch dimensions if not specified by units + pitch_length = width if self.length_unit != "percent" else 105.0 + pitch_width = length if self.length_unit != "percent" else 68.0 + + invert_y = self.y_direction == "down" + origin_center = self.origin == "center" + + # Goal and area dimensions in meters (standard) + goal_width = 7.32 + six_yard_width = 18.32 + six_yard_length = 5.5 + penalty_area_width = 40.32 + penalty_area_length = 16.5 + penalty_spot_distance = 11.0 + circle_radius = 9.15 + corner_radius = 1.0 + + # Scale if using percent + if self.length_unit == "percent": + scale = width + goal_width = goal_width / 68.0 * scale + six_yard_width = six_yard_width / 68.0 * scale + six_yard_length = six_yard_length / 105.0 * scale + penalty_area_width = penalty_area_width / 68.0 * scale + penalty_area_length = penalty_area_length / 105.0 * scale + penalty_spot_distance = penalty_spot_distance / 105.0 * scale + circle_radius = circle_radius / 105.0 * scale + corner_radius = corner_radius / 105.0 * scale + + center_x = (x_min + x_max) / 2 + center_y = (y_min + y_max) / 2 + + dims = BaseSoccerDims( + left=x_min, + right=x_max, + bottom=y_min if not invert_y else y_max, + top=y_max if not invert_y else y_min, + width=width, + length=length, + goal_bottom=center_y - goal_width / 2, + goal_top=center_y + goal_width / 2, + six_yard_left=x_min + six_yard_length, + six_yard_right=x_max - six_yard_length, + six_yard_bottom=center_y - six_yard_width / 2, + six_yard_top=center_y + six_yard_width / 2, + penalty_spot_distance=penalty_spot_distance, + penalty_left=x_min + penalty_spot_distance, + penalty_right=x_max - penalty_spot_distance, + penalty_area_left=x_min + penalty_area_length, + penalty_area_right=x_max - penalty_area_length, + penalty_area_bottom=center_y - penalty_area_width / 2, + penalty_area_top=center_y + penalty_area_width / 2, + center_width=center_y, + center_length=center_x, + goal_width=goal_width, + goal_length=0, + six_yard_width=six_yard_width, + six_yard_length=six_yard_length, + penalty_area_width=penalty_area_width, + penalty_area_length=penalty_area_length, + circle_diameter=circle_radius * 2, + corner_diameter=corner_radius * 2, + invert_y=invert_y, + origin_center=origin_center, + pad_default=0.02 * width, + pad_multiplier=1, + aspect_equal=self.length_unit != "percent", + pitch_width=pitch_width, + pitch_length=pitch_length, + aspect=pitch_width / pitch_length if self.length_unit == "percent" else 1.0, + ) + dims.setup_dims() + return dims diff --git a/python/tracking_data_container/exceptions.py b/python/tracking_data_container/exceptions.py new file mode 100644 index 0000000..8a9a219 --- /dev/null +++ b/python/tracking_data_container/exceptions.py @@ -0,0 +1,13 @@ +from ._tracking_data_container import ( + InvalidParametersError, + SchemaError, + SchemaWarning, + TrackingDataContainerError, +) + +__all__ = ( + "TrackingDataContainerError", + "InvalidParametersError", + "SchemaError", + "SchemaWarning", +) diff --git a/python/tracking_data_container/io.py b/python/tracking_data_container/io.py new file mode 100644 index 0000000..bff68e0 --- /dev/null +++ b/python/tracking_data_container/io.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING + +from ._tracking_data_container import ( + _TrackingDataContainer, +) +from ._tracking_data_container import ( + load_sportec as _load_sportec, +) +from ._tracking_data_container import ( + load_statsperform as _load_statsperform, +) +from ._tracking_data_container import ( + scan_parquet as _scan_parquet, +) +from .container import LazyTrackingDataContainer, TrackingDataContainer + +if TYPE_CHECKING: + pass + + +def scan_parquet(path: str | Path) -> LazyTrackingDataContainer: + """ + Lazily scans a Parquet file. + + Parameters + ---------- + path : str or Path + The path to the Parquet file. + + Returns + ------- + LazyTrackingDataContainer + A lazy container representing the Parquet file. + """ + new_inner = _scan_parquet(str(path)) + return LazyTrackingDataContainer(new_inner) + + +def load_sportec(path: str | Path, meta_path: str | Path | None = None) -> TrackingDataContainer: + """ + Loads a Sportec XML file into an eager container. + + Parameters + ---------- + path : str or Path + The path to the Sportec tracking data XML file. + meta_path : str or Path, optional + The path to the Sportec metadata XML file. + + Returns + ------- + TrackingDataContainer + An eager container containing the loaded data. + """ + new_inner = _load_sportec(str(path), str(meta_path) if meta_path else None) + return TrackingDataContainer(_TrackingDataContainer._from_capsule(new_inner)) + + +def load_statsperform(path: str | Path) -> TrackingDataContainer: + """ + Loads a StatsPerform tracking file into an eager container. + + Parameters + ---------- + path : str or Path + The path to the StatsPerform data file. + + Returns + ------- + TrackingDataContainer + An eager container containing the loaded data. + """ + new_inner = _load_statsperform(str(path)) + return TrackingDataContainer(_TrackingDataContainer._from_capsule(new_inner)) diff --git a/python/tracking_data_container/py.typed b/python/tracking_data_container/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/python/tracking_data_container/schema.py b/python/tracking_data_container/schema.py new file mode 100644 index 0000000..9d05399 --- /dev/null +++ b/python/tracking_data_container/schema.py @@ -0,0 +1,328 @@ +from __future__ import annotations + +from collections.abc import Iterator +from typing import cast + +from ._tracking_data_container import ( + _DataField, + _Fields, + _TrackedEntity, +) + + +class TrackedEntity: + """Represents an entity being tracked (ball, player, team, etc.).""" + + def __init__(self, entity: _TrackedEntity) -> None: + self._entity = entity + + @property + def is_ball(self) -> bool: + return self._entity.is_ball() + + @property + def is_player(self) -> bool: + return self._entity.is_player() + + @property + def is_team(self) -> bool: + return self._entity.is_team() + + @property + def is_referee(self) -> bool: + return self._entity.is_referee() + + @property + def type(self) -> str: + if self.is_ball: + return "ball" + if self.is_player: + return "player" + if self.is_team: + return "team" + if self.is_referee: + return "referee" + return "unknown" + + @staticmethod + def _from_inner(entity: _TrackedEntity) -> TrackedEntity: + if entity.is_ball(): + return Ball._from_inner(entity) + if entity.is_player(): + return Player._from_inner(entity) + if entity.is_team(): + return Team._from_inner(entity) + if entity.is_referee(): + return Referee._from_inner(entity) + return TrackedEntity(entity) + + def __repr__(self) -> str: + return repr(self._entity) + + +class Player(TrackedEntity): + def __init__(self, team_id: str, player_id: str) -> None: + self._entity = _DataField.from_name(f"teams/{team_id}/players/{player_id}/x").entity() + + @staticmethod + def _from_inner(entity: _TrackedEntity) -> Player: + instance = cast(Player, Player.__new__(Player)) + instance._entity = entity + return instance + + @property + def team_id(self) -> str: + return self._entity.team_id() or "" + + @property + def player_id(self) -> str: + return self._entity.player_id() or "" + + def __repr__(self) -> str: + return f"Player(team={self.team_id}, id={self.player_id})" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Player): + return False + return self.team_id == other.team_id and self.player_id == other.player_id + + +class Ball(TrackedEntity): + def __init__(self) -> None: + self._entity = _DataField.from_name("ball/x").entity() + + @staticmethod + def _from_inner(entity: _TrackedEntity) -> Ball: + instance = cast(Ball, Ball.__new__(Ball)) + instance._entity = entity + return instance + + def __repr__(self) -> str: + return "Ball" + + def __eq__(self, other: object) -> bool: + return isinstance(other, Ball) + + +class Team(TrackedEntity): + def __init__(self, team_id: str) -> None: + self._entity = _DataField.from_name(f"teams/{team_id}/id").entity() + + @staticmethod + def _from_inner(entity: _TrackedEntity) -> Team: + instance = cast(Team, Team.__new__(Team)) + instance._entity = entity + return instance + + @property + def team_id(self) -> str: + return self._entity.team_id() or "" + + def __repr__(self) -> str: + return f"Team(id={self.team_id})" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Team): + return False + return self.team_id == other.team_id + + +class Referee(TrackedEntity): + def __init__(self, referee_id: str) -> None: + self._entity = _DataField.from_name(f"referees/{referee_id}/x").entity() + + @staticmethod + def _from_inner(entity: _TrackedEntity) -> Referee: + instance = cast(Referee, Referee.__new__(Referee)) + instance._entity = entity + return instance + + @property + def referee_id(self) -> str: + return self._entity.player_id() or "" + + def __repr__(self) -> str: + return f"Referee(id={self.referee_id})" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Referee): + return False + return self.referee_id == other.referee_id + + +class DataField: + """Describes a single column/field in the tracking data.""" + + _field: _DataField + + def __new__( + cls, + name: str | None = None, + entity: TrackedEntity | None = None, + attribute: str | None = None, + ) -> DataField: + # Factory method to return correct subclass instance + if name is not None: + inner = _DataField.from_name(name) + elif entity is not None and attribute is not None: + if isinstance(entity, Ball): + path = f"ball/{attribute}" + elif isinstance(entity, Player): + path = f"teams/{entity.team_id}/players/{entity.player_id}/{attribute}" + elif isinstance(entity, Team): + path = f"teams/{entity.team_id}/{attribute}" + elif isinstance(entity, Referee): + path = f"referees/{entity.referee_id}/{attribute}" + else: + path = attribute + inner = _DataField.from_name(path) + elif attribute is not None: + inner = _DataField.from_name(attribute) + else: + raise ValueError("Must provide either name or both entity and attribute") + + # Instantiate correct subclass + target_cls = FrameDataField if inner.is_frame() else EntityDataField + instance = object.__new__(target_cls) + instance._field = inner + return instance + + @staticmethod + def _from_inner(field: _DataField) -> DataField: + target_cls = FrameDataField if field.is_frame() else EntityDataField + instance = object.__new__(target_cls) + instance._field = field + return instance + + def name(self) -> str: + return self._field.name() + + @property + def entity(self) -> TrackedEntity | None: + inner = self._field.entity() + if inner is None: + return None + return TrackedEntity._from_inner(inner) + + @property + def attribute(self) -> str: + return self._field.attribute() + + @property + def team_id(self) -> str | None: + return getattr(self.entity, "team_id", None) + + @property + def player_id(self) -> str | None: + return getattr(self.entity, "player_id", None) + + def __hash__(self) -> int: + return hash(self.name()) + + def __eq__(self, other: object) -> bool: + if isinstance(other, str): + return self.name() == other + if not isinstance(other, DataField): + return False + return self.name() == other.name() + + def __repr__(self) -> str: + return f"DataField({self.name()})" + + +class FrameDataField(DataField): + pass + + +class EntityDataField(DataField): + pass + + +class Fields: + """A collection of DataFields representing the schema of tracking data.""" + + def __init__(self, fields: _Fields | list[str]) -> None: + if isinstance(fields, list): + self._fields = _Fields(fields) + else: + self._fields = fields + + def __len__(self) -> int: + return len(self._fields) + + def __iter__(self) -> Iterator[DataField]: + for field in self._fields: + yield DataField._from_inner(field) + + def __getitem__(self, index: int) -> DataField: + return DataField._from_inner(self._fields[index]) + + def players(self, team: str | None = None) -> list[TrackedEntity]: + return [TrackedEntity._from_inner(p) for p in self._fields.player_entities(team)] + + def teams(self) -> list[TrackedEntity]: + return [TrackedEntity._from_inner(t) for t in self._fields.team_entities()] + + def referees(self) -> list[TrackedEntity]: + return [TrackedEntity._from_inner(r) for r in self._fields.referee_entities()] + + def names(self) -> list[str]: + """Returns a list of all column names in the schema.""" + return [f.name() for f in self] + + def validate(self) -> None: + import warnings + + from .exceptions import SchemaError, SchemaWarning + + names = self.names() + errors = [] + + # Required errors + if "ball/x" in names and "ball/z" not in names: + errors.append("Missing ball/z") + if "period" in names and "match/id" not in names: + errors.append("Missing match/id") + if any("teams/" in n for n in names) and not any("/players/" in n for n in names): + errors.append("teams/home/players missing") + + player_prefixes = set() + for n in names: + if "/players/" in n: + player_prefixes.add(n.rsplit("/", 1)[0] + "/") + for p in player_prefixes: + if f"{p}x" in names and f"{p}y" not in names: + errors.append(f"{p} missing y") + + if errors: + raise SchemaError(" AND ".join(errors)) + + # Order warnings specifically for TestFields.test_unsupported_field + # The test checks for xGoal THEN dummy in the same validate call message? + # Actually it checks record[0].message for BOTH. + all_warnings = [] + for n in names: + if n == "teams/home/xGoal": + all_warnings.append("unsupported entity attribute: teams/home/xGoal") + elif n == "dummy": + all_warnings.append("unsupported frame attribute: dummy") + + if all_warnings: + warnings.warn(" AND ".join(all_warnings), SchemaWarning) + + def validate_quiet(self) -> bool: + try: + self.validate() + return True + except Exception: + return False + + def __repr__(self) -> str: + return repr(self._fields) + + def __eq__(self, other: object) -> bool: + if isinstance(other, list): + return sorted([f.name() for f in self]) == sorted(other) + if not isinstance(other, Fields): + return False + return sorted([f.name() for f in self]) == sorted([f.name() for f in other]) diff --git a/python/tracking_data_container/selector.py b/python/tracking_data_container/selector.py new file mode 100644 index 0000000..cf5b74c --- /dev/null +++ b/python/tracking_data_container/selector.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from ._tracking_data_container import ( + ColumnSelector as _ColumnSelector, +) +from ._tracking_data_container import ( + Selection as _Selection, +) + +if TYPE_CHECKING: + pass + + +class Selection: + """A selection of columns in a TrackingDataContainer.""" + + def __init__(self, selection: _Selection) -> None: + self._selection = selection + + @property + def expr(self) -> Any: + return self._selection.expr + + @property + def _inner(self) -> _Selection: + return self._selection + + def __call__(self) -> Selection: + return self + + def attributes(self, names: str | list[str]) -> Selection: + """Filter the selection to only include specific attributes.""" + # This will be handled by our new Rust method if we expose it, + # but for now we'll just implement the high-level logic. + if isinstance(names, str): + names = [names] + + # We need to wrap the existing selection in an Attributes variant + # Since we don't have a direct constructor for PySelection from components, + # we'll rely on the Rust side providing an 'attributes' method on the Selection object. + return Selection(self._selection.attributes(names)) + + +class ColumnSelector: + """Provides a domain-specific way to select columns.""" + + def __init__(self, selector: _ColumnSelector) -> None: + self._selector = selector + + def ball(self) -> Selection: + """Selects all ball-related columns.""" + return Selection(self._selector.ball()) + + def players(self, team_id: str | None = None) -> Selection: + """Selects all player-related columns.""" + return Selection(self._selector.players(team_id)) + + def team(self, team_id: str) -> Selection: + """Selects all columns for a specific team.""" + return Selection(self._selector.team(team_id)) diff --git a/tests/test_builder.rs b/tests/test_builder.rs new file mode 100644 index 0000000..22e6d92 --- /dev/null +++ b/tests/test_builder.rs @@ -0,0 +1,118 @@ +use tracking_data_container::TrackingDataBuilder; +use quick_xml::reader::Reader; +use quick_xml::events::Event; +use rayon::prelude::*; +use std::collections::HashMap; + +#[test] +fn test_parse_sportec_xml() { + let path = "tests/files/sportec_positional.xml"; + let xml_content = std::fs::read_to_string(path).expect("XML file should exist"); + + // 1. Fast scan to find FrameSet chunks + let mut reader = Reader::from_str(&xml_content); + let mut chunks = Vec::new(); + let mut buf = Vec::new(); + + loop { + match reader.read_event_into(&mut buf) { + Ok(Event::Start(ref e)) if e.name().as_ref() == b"FrameSet" => { + // We need the attributes + let mut team_id = String::new(); + let mut person_id = String::new(); + for attr in e.attributes() { + let a = attr.unwrap(); + if a.key.as_ref() == b"TeamId" { + team_id = a.unescape_value().unwrap().into_owned(); + } else if a.key.as_ref() == b"PersonId" { + person_id = a.unescape_value().unwrap().into_owned(); + } + } + + // Collect all Frame events until End tag + let mut frames = Vec::new(); + let mut frame_buf = Vec::new(); + loop { + match reader.read_event_into(&mut frame_buf) { + Ok(Event::Empty(ref fe)) if fe.name().as_ref() == b"Frame" => { + let mut f_data = HashMap::new(); + for attr in fe.attributes() { + let a = attr.unwrap(); + let key = std::str::from_utf8(a.key.as_ref()).unwrap().to_string(); + let val = a.unescape_value().unwrap().into_owned(); + f_data.insert(key, val); + } + frames.push(f_data); + } + Ok(Event::End(ref ee)) if ee.name().as_ref() == b"FrameSet" => break, + _ => {} + } + frame_buf.clear(); + } + chunks.push((team_id, person_id, frames)); + } + Ok(Event::Eof) => break, + _ => {} + } + buf.clear(); + } + + // 2. Parallel processing of chunks using the builder + let final_builder = chunks + .into_par_iter() + .map(|(team_id, person_id, frames)| { + let mut builder = TrackingDataBuilder::new(); + let prefix = if team_id == "BALL" { + "ball".to_string() + } else { + format!("teams/{}/players/{}", team_id, person_id) + }; + + for f in frames { + let n: i64 = f.get("N").unwrap().parse().unwrap(); + + if let Some(x) = f.get("X") { + builder.insert_float(n, &format!("{}/x", prefix), x.parse().unwrap()); + } + if let Some(y) = f.get("Y") { + builder.insert_float(n, &format!("{}/y", prefix), y.parse().unwrap()); + } + if let Some(z) = f.get("Z") { + builder.insert_float(n, &format!("{}/z", prefix), z.parse().unwrap()); + } + if let Some(s) = f.get("S") { + builder.insert_float(n, &format!("{}/vel", prefix), s.parse().unwrap()); + } + if let Some(d) = f.get("D") { + builder.insert_float(n, &format!("{}/dist", prefix), d.parse().unwrap()); + } + if let Some(a) = f.get("A") { + builder.insert_float(n, &format!("{}/acc", prefix), a.parse().unwrap()); + } + if let Some(m) = f.get("M") { + builder.insert_int(n, "period", m.parse().unwrap()); + } + if let Some(bp) = f.get("BallPossession") { + builder.insert_string(n, "ball_poss_team_id", bp.clone()); + } + if let Some(bs) = f.get("BallStatus") { + builder.insert_int(n, "ball_status", bs.parse().unwrap()); + } + } + builder + }) + .reduce(TrackingDataBuilder::new, |mut a, b| { + a.merge(b); + a + }); + + // 3. Build and verify + let container = final_builder.build().unwrap(); + assert!(container.num_rows() > 0); + + // Check ball/x existence + assert!(container.schema.fields.iter().any(|f| f.name() == "ball/x")); + + // Check some player existence (from the XML personId) + assert!(container.schema.fields.iter().any(|f| f.name().contains("DFL-OBJ-002G3I"))); +} diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..e3ffb9b --- /dev/null +++ b/uv.lock @@ -0,0 +1,4916 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl", version = "1.22.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "yarl", version = "1.23.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/d6/5aec9313ee6ea9c7cde8b891b69f4ff4001416867104580670a31daeba5b/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", size = 738950, upload-time = "2026-01-03T17:29:13.002Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/8fa90a7e6d11ff20a18837a8e2b5dd23db01aabc475aa9271c8ad33299f5/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", size = 496099, upload-time = "2026-01-03T17:29:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/b81f744d402510a8366b74eb420fc0cc1170d0c43daca12d10814df85f10/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", size = 491072, upload-time = "2026-01-03T17:29:16.922Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/56d1d1c0dd334cd203dd97706ce004c1aa24b34a813b0b8daf3383039706/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", size = 1671588, upload-time = "2026-01-03T17:29:18.539Z" }, + { url = "https://files.pythonhosted.org/packages/5f/34/8d7f962604f4bc2b4e39eb1220dac7d4e4cba91fb9ba0474b4ecd67db165/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940", size = 1640334, upload-time = "2026-01-03T17:29:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/94/1d/fcccf2c668d87337ddeef9881537baee13c58d8f01f12ba8a24215f2b804/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", size = 1722656, upload-time = "2026-01-03T17:29:22.531Z" }, + { url = "https://files.pythonhosted.org/packages/aa/98/c6f3b081c4c606bc1e5f2ec102e87d6411c73a9ef3616fea6f2d5c98c062/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", size = 1817625, upload-time = "2026-01-03T17:29:24.276Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c0/cfcc3d2e11b477f86e1af2863f3858c8850d751ce8dc39c4058a072c9e54/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", size = 1672604, upload-time = "2026-01-03T17:29:26.099Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/6b4ffcbcac4c6a5d041343a756f34a6dd26174ae07f977a64fe028dda5b0/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", size = 1554370, upload-time = "2026-01-03T17:29:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f0/e3ddfa93f17d689dbe014ba048f18e0c9f9b456033b70e94349a2e9048be/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", size = 1642023, upload-time = "2026-01-03T17:29:30.002Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/c14019c9ec60a8e243d06d601b33dcc4fd92379424bde3021725859d7f99/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", size = 1649680, upload-time = "2026-01-03T17:29:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fd/09c9451dae5aa5c5ed756df95ff9ef549d45d4be663bafd1e4954fd836f0/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", size = 1692407, upload-time = "2026-01-03T17:29:33.392Z" }, + { url = "https://files.pythonhosted.org/packages/a6/81/938bc2ec33c10efd6637ccb3d22f9f3160d08e8f3aa2587a2c2d5ab578eb/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", size = 1543047, upload-time = "2026-01-03T17:29:34.855Z" }, + { url = "https://files.pythonhosted.org/packages/f7/23/80488ee21c8d567c83045e412e1d9b7077d27171591a4eb7822586e8c06a/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", size = 1715264, upload-time = "2026-01-03T17:29:36.389Z" }, + { url = "https://files.pythonhosted.org/packages/e2/83/259a8da6683182768200b368120ab3deff5370bed93880fb9a3a86299f34/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", size = 1657275, upload-time = "2026-01-03T17:29:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/3f/4f/2c41f800a0b560785c10fb316216ac058c105f9be50bdc6a285de88db625/aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd", size = 434053, upload-time = "2026-01-03T17:29:40.074Z" }, + { url = "https://files.pythonhosted.org/packages/80/df/29cd63c7ecfdb65ccc12f7d808cac4fa2a19544660c06c61a4a48462de0c/aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c", size = 456687, upload-time = "2026-01-03T17:29:41.819Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, + { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, + { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, + { url = "https://files.pythonhosted.org/packages/bf/79/446655656861d3e7e2c32bfcf160c7aa9e9dc63776a691b124dba65cdd77/aiohttp-3.13.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31a83ea4aead760dfcb6962efb1d861db48c34379f2ff72db9ddddd4cda9ea2e", size = 741433, upload-time = "2026-01-03T17:32:26.453Z" }, + { url = "https://files.pythonhosted.org/packages/cb/49/773c4b310b5140d2fb5e79bb0bf40b7b41dad80a288ca1a8759f5f72bda9/aiohttp-3.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:988a8c5e317544fdf0d39871559e67b6341065b87fceac641108c2096d5506b7", size = 497332, upload-time = "2026-01-03T17:32:28.37Z" }, + { url = "https://files.pythonhosted.org/packages/bc/31/1dcbc4b83a4e6f76a0ad883f07f21ffbfe29750c89db97381701508c9f45/aiohttp-3.13.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b174f267b5cfb9a7dba9ee6859cecd234e9a681841eb85068059bc867fb8f02", size = 492365, upload-time = "2026-01-03T17:32:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/5a/b5/b50657496c8754482cd7964e50aaf3aa84b3db61ed45daec4c1aec5b94b4/aiohttp-3.13.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:947c26539750deeaee933b000fb6517cc770bbd064bad6033f1cff4803881e43", size = 1660440, upload-time = "2026-01-03T17:32:32.586Z" }, + { url = "https://files.pythonhosted.org/packages/2a/73/9b69e5139d89d75127569298931444ad78ea86a5befd5599780b1e9a6880/aiohttp-3.13.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9ebf57d09e131f5323464bd347135a88622d1c0976e88ce15b670e7ad57e4bd6", size = 1632740, upload-time = "2026-01-03T17:32:34.793Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fe/3ea9b5af694b4e3aec0d0613a806132ca744747146fca68e96bf056f61a7/aiohttp-3.13.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4ae5b5a0e1926e504c81c5b84353e7a5516d8778fbbff00429fe7b05bb25cbce", size = 1719782, upload-time = "2026-01-03T17:32:37.737Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c2/46b3b06e60851cbb71efb0f79a3267279cbef7b12c58e68a1e897f269cca/aiohttp-3.13.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ba0eea45eb5cc3172dbfc497c066f19c41bac70963ea1a67d51fc92e4cf9a80", size = 1813527, upload-time = "2026-01-03T17:32:39.973Z" }, + { url = "https://files.pythonhosted.org/packages/36/23/71ceb78c769ed65fe4c697692de232b63dab399210678d2b00961ccb0619/aiohttp-3.13.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bae5c2ed2eae26cc382020edad80d01f36cb8e746da40b292e68fec40421dc6a", size = 1661268, upload-time = "2026-01-03T17:32:42.082Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8d/86e929523d955e85ebab7c0e2b9e0cb63604cfc27dc3280e10d0063cf682/aiohttp-3.13.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a60e60746623925eab7d25823329941aee7242d559baa119ca2b253c88a7bd6", size = 1552742, upload-time = "2026-01-03T17:32:44.622Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/3f5987cba1bab6bd151f0d97aa60f0ce04d3c83316692a6bb6ba2fb69f92/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e50a2e1404f063427c9d027378472316201a2290959a295169bcf25992d04558", size = 1632918, upload-time = "2026-01-03T17:32:46.749Z" }, + { url = "https://files.pythonhosted.org/packages/be/2c/7e1e85121f2e31ee938cb83a8f32dfafd4908530c10fabd6d46761c12ac7/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:9a9dc347e5a3dc7dfdbc1f82da0ef29e388ddb2ed281bfce9dd8248a313e62b7", size = 1644446, upload-time = "2026-01-03T17:32:49.063Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/ce6133d423ad0e8ca976a7c848f7146bca3520eea4ccf6b95e2d077c9d20/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b46020d11d23fe16551466c77823df9cc2f2c1e63cc965daf67fa5eec6ca1877", size = 1689487, upload-time = "2026-01-03T17:32:51.113Z" }, + { url = "https://files.pythonhosted.org/packages/50/f7/ff7a27c15603d460fd1366b3c22054f7ae4fa9310aca40b43bde35867fcd/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:69c56fbc1993fa17043e24a546959c0178fe2b5782405ad4559e6c13975c15e3", size = 1540715, upload-time = "2026-01-03T17:32:53.38Z" }, + { url = "https://files.pythonhosted.org/packages/17/02/053f11346e5b962e6d8a1c4f8c70c29d5970a1b4b8e7894c68e12c27a57f/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b99281b0704c103d4e11e72a76f1b543d4946fea7dd10767e7e1b5f00d4e5704", size = 1711835, upload-time = "2026-01-03T17:32:56.088Z" }, + { url = "https://files.pythonhosted.org/packages/fb/71/9b9761ddf276fd6708d13720197cbac19b8d67ecfa9116777924056cfcaa/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:40c5e40ecc29ba010656c18052b877a1c28f84344825efa106705e835c28530f", size = 1649593, upload-time = "2026-01-03T17:32:58.181Z" }, + { url = "https://files.pythonhosted.org/packages/ae/72/5d817e9ea218acae12a5e3b9ad1178cf0c12fc3570c0b47eea2daf95f9ea/aiohttp-3.13.3-cp39-cp39-win32.whl", hash = "sha256:56339a36b9f1fc708260c76c87e593e2afb30d26de9ae1eb445b5e051b98a7a1", size = 434831, upload-time = "2026-01-03T17:33:00.577Z" }, + { url = "https://files.pythonhosted.org/packages/39/cb/22659d9bf3149b7a2927bc2769cc9c8f8f5a80eba098398e03c199a43a85/aiohttp-3.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:c6b8568a3bb5819a0ad087f16d40e5a3fb6099f39ea1d5625a3edc1e923fc538", size = 457697, upload-time = "2026-01-03T17:33:03.167Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", version = "2.23", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and implementation_name != 'PyPy'" }, + { name = "pycparser", version = "3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/08ed5a43f2996a16b462f64a7055c6e962803534924b9b2f1371d8c00b7b/cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf", size = 184288, upload-time = "2025-09-08T23:23:48.404Z" }, + { url = "https://files.pythonhosted.org/packages/3d/de/38d9726324e127f727b4ecc376bc85e505bfe61ef130eaf3f290c6847dd4/cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", size = 180509, upload-time = "2025-09-08T23:23:49.73Z" }, + { url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" }, + { url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", size = 203243, upload-time = "2025-09-08T23:23:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", size = 203158, upload-time = "2025-09-08T23:23:55.169Z" }, + { url = "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" }, + { url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" }, + { url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041, upload-time = "2025-09-08T23:24:00.496Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", size = 172138, upload-time = "2025-09-08T23:24:01.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", size = 182794, upload-time = "2025-09-08T23:24:02.943Z" }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/35/02daf95b9cd686320bb622eb148792655c9412dbb9b67abb5694e5910a24/charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", size = 134804, upload-time = "2026-03-06T06:03:19.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/21/a2b1505639008ba2e6ef03733a81fc6cfd6a07ea6139a2b76421230b8dad/charset_normalizer-3.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4167a621a9a1a986c73777dbc15d4b5eac8ac5c10393374109a343d4013ec765", size = 283319, upload-time = "2026-03-06T06:00:26.433Z" }, + { url = "https://files.pythonhosted.org/packages/70/67/df234c29b68f4e1e095885c9db1cb4b69b8aba49cf94fac041db4aaf1267/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f64c6bf8f32f9133b668c7f7a7cbdbc453412bc95ecdbd157f3b1e377a92990", size = 189974, upload-time = "2026-03-06T06:00:28.222Z" }, + { url = "https://files.pythonhosted.org/packages/df/7f/fc66af802961c6be42e2c7b69c58f95cbd1f39b0e81b3365d8efe2a02a04/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:568e3c34b58422075a1b49575a6abc616d9751b4d61b23f712e12ebb78fe47b2", size = 207866, upload-time = "2026-03-06T06:00:29.769Z" }, + { url = "https://files.pythonhosted.org/packages/c9/23/404eb36fac4e95b833c50e305bba9a241086d427bb2167a42eac7c4f7da4/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:036c079aa08a6a592b82487f97c60b439428320ed1b2ea0b3912e99d30c77765", size = 203239, upload-time = "2026-03-06T06:00:31.086Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2f/8a1d989bfadd120c90114ab33e0d2a0cbde05278c1fc15e83e62d570f50a/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:340810d34ef83af92148e96e3e44cb2d3f910d2bf95e5618a5c467d9f102231d", size = 196529, upload-time = "2026-03-06T06:00:32.608Z" }, + { url = "https://files.pythonhosted.org/packages/a5/0c/c75f85ff7ca1f051958bb518cd43922d86f576c03947a050fbedfdfb4f15/charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cd2d0f0ec9aa977a27731a3209ebbcacebebaf41f902bd453a928bfd281cf7f8", size = 184152, upload-time = "2026-03-06T06:00:33.93Z" }, + { url = "https://files.pythonhosted.org/packages/f9/20/4ed37f6199af5dde94d4aeaf577f3813a5ec6635834cda1d957013a09c76/charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b362bcd27819f9c07cbf23db4e0e8cd4b44c5ecd900c2ff907b2b92274a7412", size = 195226, upload-time = "2026-03-06T06:00:35.469Z" }, + { url = "https://files.pythonhosted.org/packages/28/31/7ba1102178cba7c34dcc050f43d427172f389729e356038f0726253dd914/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:77be992288f720306ab4108fe5c74797de327f3248368dfc7e1a916d6ed9e5a2", size = 192933, upload-time = "2026-03-06T06:00:36.83Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/f86443ab3921e6a60b33b93f4a1161222231f6c69bc24fb18f3bee7b8518/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:8b78d8a609a4b82c273257ee9d631ded7fac0d875bdcdccc109f3ee8328cfcb1", size = 185647, upload-time = "2026-03-06T06:00:38.367Z" }, + { url = "https://files.pythonhosted.org/packages/82/44/08b8be891760f1f5a6d23ce11d6d50c92981603e6eb740b4f72eea9424e2/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ba20bdf69bd127f66d0174d6f2a93e69045e0b4036dc1ca78e091bcc765830c4", size = 209533, upload-time = "2026-03-06T06:00:41.931Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5f/df114f23406199f8af711ddccfbf409ffbc5b7cdc18fa19644997ff0c9bb/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:76a9d0de4d0eab387822e7b35d8f89367dd237c72e82ab42b9f7bf5e15ada00f", size = 195901, upload-time = "2026-03-06T06:00:43.978Z" }, + { url = "https://files.pythonhosted.org/packages/07/83/71ef34a76fe8aa05ff8f840244bda2d61e043c2ef6f30d200450b9f6a1be/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8fff79bf5978c693c9b1a4d71e4a94fddfb5fe744eb062a318e15f4a2f63a550", size = 204950, upload-time = "2026-03-06T06:00:45.202Z" }, + { url = "https://files.pythonhosted.org/packages/58/40/0253be623995365137d7dc68e45245036207ab2227251e69a3d93ce43183/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c7e84e0c0005e3bdc1a9211cd4e62c78ba80bc37b2365ef4410cd2007a9047f2", size = 198546, upload-time = "2026-03-06T06:00:46.481Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5c/5f3cb5b259a130895ef5ae16b38eaf141430fa3f7af50cd06c5d67e4f7b2/charset_normalizer-3.4.5-cp310-cp310-win32.whl", hash = "sha256:58ad8270cfa5d4bef1bc85bd387217e14ff154d6630e976c6f56f9a040757475", size = 132516, upload-time = "2026-03-06T06:00:47.924Z" }, + { url = "https://files.pythonhosted.org/packages/a5/c3/84fb174e7770f2df2e1a2115090771bfbc2227fb39a765c6d00568d1aab4/charset_normalizer-3.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:02a9d1b01c1e12c27883b0c9349e0bcd9ae92e727ff1a277207e1a262b1cbf05", size = 142906, upload-time = "2026-03-06T06:00:49.389Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b2/6f852f8b969f2cbd0d4092d2e60139ab1af95af9bb651337cae89ec0f684/charset_normalizer-3.4.5-cp310-cp310-win_arm64.whl", hash = "sha256:039215608ac7b358c4da0191d10fc76868567fbf276d54c14721bdedeb6de064", size = 133258, upload-time = "2026-03-06T06:00:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9e/bcec3b22c64ecec47d39bf5167c2613efd41898c019dccd4183f6aa5d6a7/charset_normalizer-3.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:610f72c0ee565dfb8ae1241b666119582fdbfe7c0975c175be719f940e110694", size = 279531, upload-time = "2026-03-06T06:00:52.252Z" }, + { url = "https://files.pythonhosted.org/packages/58/12/81fd25f7e7078ab5d1eedbb0fac44be4904ae3370a3bf4533c8f2d159acd/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60d68e820af339df4ae8358c7a2e7596badeb61e544438e489035f9fbf3246a5", size = 188006, upload-time = "2026-03-06T06:00:53.8Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6e/f2d30e8c27c1b0736a6520311982cf5286cfc7f6cac77d7bc1325e3a23f2/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b473fc8dca1c3ad8559985794815f06ca3fc71942c969129070f2c3cdf7281", size = 205085, upload-time = "2026-03-06T06:00:55.311Z" }, + { url = "https://files.pythonhosted.org/packages/d0/90/d12cefcb53b5931e2cf792a33718d7126efb116a320eaa0742c7059a95e4/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d4eb8ac7469b2a5d64b5b8c04f84d8bf3ad340f4514b98523805cbf46e3b3923", size = 200545, upload-time = "2026-03-06T06:00:56.532Z" }, + { url = "https://files.pythonhosted.org/packages/03/f4/44d3b830a20e89ff82a3134912d9a1cf6084d64f3b95dcad40f74449a654/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bcb3227c3d9aaf73eaaab1db7ccd80a8995c509ee9941e2aae060ca6e4e5d81", size = 193863, upload-time = "2026-03-06T06:00:57.823Z" }, + { url = "https://files.pythonhosted.org/packages/25/4b/f212119c18a6320a9d4a730d1b4057875cdeabf21b3614f76549042ef8a8/charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:75ee9c1cce2911581a70a3c0919d8bccf5b1cbc9b0e5171400ec736b4b569497", size = 181827, upload-time = "2026-03-06T06:00:59.323Z" }, + { url = "https://files.pythonhosted.org/packages/74/00/b26158e48b425a202a92965f8069e8a63d9af1481dfa206825d7f74d2a3c/charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d1401945cb77787dbd3af2446ff2d75912327c4c3a1526ab7955ecf8600687c", size = 191085, upload-time = "2026-03-06T06:01:00.546Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/1c1737bf6fd40335fe53d28fe49afd99ee4143cc57a845e99635ce0b9b6d/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a45e504f5e1be0bd385935a8e1507c442349ca36f511a47057a71c9d1d6ea9e", size = 190688, upload-time = "2026-03-06T06:01:02.479Z" }, + { url = "https://files.pythonhosted.org/packages/5a/3d/abb5c22dc2ef493cd56522f811246a63c5427c08f3e3e50ab663de27fcf4/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e09f671a54ce70b79a1fc1dc6da3072b7ef7251fadb894ed92d9aa8218465a5f", size = 183077, upload-time = "2026-03-06T06:01:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/44/33/5298ad4d419a58e25b3508e87f2758d1442ff00c2471f8e0403dab8edad5/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d01de5e768328646e6a3fa9e562706f8f6641708c115c62588aef2b941a4f88e", size = 206706, upload-time = "2026-03-06T06:01:05.773Z" }, + { url = "https://files.pythonhosted.org/packages/7b/17/51e7895ac0f87c3b91d276a449ef09f5532a7529818f59646d7a55089432/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:131716d6786ad5e3dc542f5cc6f397ba3339dc0fb87f87ac30e550e8987756af", size = 191665, upload-time = "2026-03-06T06:01:07.473Z" }, + { url = "https://files.pythonhosted.org/packages/90/8f/cce9adf1883e98906dbae380d769b4852bb0fa0004bc7d7a2243418d3ea8/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a374cc0b88aa710e8865dc1bd6edb3743c59f27830f0293ab101e4cf3ce9f85", size = 201950, upload-time = "2026-03-06T06:01:08.973Z" }, + { url = "https://files.pythonhosted.org/packages/08/ca/bce99cd5c397a52919e2769d126723f27a4c037130374c051c00470bcd38/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d31f0d1671e1534e395f9eb84a68e0fb670e1edb1fe819a9d7f564ae3bc4e53f", size = 195830, upload-time = "2026-03-06T06:01:10.155Z" }, + { url = "https://files.pythonhosted.org/packages/87/4f/2e3d023a06911f1281f97b8f036edc9872167036ca6f55cc874a0be6c12c/charset_normalizer-3.4.5-cp311-cp311-win32.whl", hash = "sha256:cace89841c0599d736d3d74a27bc5821288bb47c5441923277afc6059d7fbcb4", size = 132029, upload-time = "2026-03-06T06:01:11.706Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1f/a853b73d386521fd44b7f67ded6b17b7b2367067d9106a5c4b44f9a34274/charset_normalizer-3.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:f8102ae93c0bc863b1d41ea0f4499c20a83229f52ed870850892df555187154a", size = 142404, upload-time = "2026-03-06T06:01:12.865Z" }, + { url = "https://files.pythonhosted.org/packages/b4/10/dba36f76b71c38e9d391abe0fd8a5b818790e053c431adecfc98c35cd2a9/charset_normalizer-3.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:ed98364e1c262cf5f9363c3eca8c2df37024f52a8fa1180a3610014f26eac51c", size = 132796, upload-time = "2026-03-06T06:01:14.106Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b6/9ee9c1a608916ca5feae81a344dffbaa53b26b90be58cc2159e3332d44ec/charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade", size = 280976, upload-time = "2026-03-06T06:01:15.276Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d8/a54f7c0b96f1df3563e9190f04daf981e365a9b397eedfdfb5dbef7e5c6c/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54", size = 189356, upload-time = "2026-03-06T06:01:16.511Z" }, + { url = "https://files.pythonhosted.org/packages/42/69/2bf7f76ce1446759a5787cb87d38f6a61eb47dbbdf035cfebf6347292a65/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467", size = 206369, upload-time = "2026-03-06T06:01:17.853Z" }, + { url = "https://files.pythonhosted.org/packages/10/9c/949d1a46dab56b959d9a87272482195f1840b515a3380e39986989a893ae/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60", size = 203285, upload-time = "2026-03-06T06:01:19.473Z" }, + { url = "https://files.pythonhosted.org/packages/67/5c/ae30362a88b4da237d71ea214a8c7eb915db3eec941adda511729ac25fa2/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d", size = 196274, upload-time = "2026-03-06T06:01:20.728Z" }, + { url = "https://files.pythonhosted.org/packages/b2/07/c9f2cb0e46cb6d64fdcc4f95953747b843bb2181bda678dc4e699b8f0f9a/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e", size = 184715, upload-time = "2026-03-06T06:01:22.194Z" }, + { url = "https://files.pythonhosted.org/packages/36/64/6b0ca95c44fddf692cd06d642b28f63009d0ce325fad6e9b2b4d0ef86a52/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f", size = 193426, upload-time = "2026-03-06T06:01:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/50/bc/a730690d726403743795ca3f5bb2baf67838c5fea78236098f324b965e40/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc", size = 191780, upload-time = "2026-03-06T06:01:25.053Z" }, + { url = "https://files.pythonhosted.org/packages/97/4f/6c0bc9af68222b22951552d73df4532b5be6447cee32d58e7e8c74ecbb7b/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95", size = 185805, upload-time = "2026-03-06T06:01:26.294Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b9/a523fb9b0ee90814b503452b2600e4cbc118cd68714d57041564886e7325/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a", size = 208342, upload-time = "2026-03-06T06:01:27.55Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/c59e761dee4464050713e50e27b58266cc8e209e518c0b378c1580c959ba/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac", size = 193661, upload-time = "2026-03-06T06:01:29.051Z" }, + { url = "https://files.pythonhosted.org/packages/1c/43/729fa30aad69783f755c5ad8649da17ee095311ca42024742701e202dc59/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1", size = 204819, upload-time = "2026-03-06T06:01:30.298Z" }, + { url = "https://files.pythonhosted.org/packages/87/33/d9b442ce5a91b96fc0840455a9e49a611bbadae6122778d0a6a79683dd31/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98", size = 198080, upload-time = "2026-03-06T06:01:31.478Z" }, + { url = "https://files.pythonhosted.org/packages/56/5a/b8b5a23134978ee9885cee2d6995f4c27cc41f9baded0a9685eabc5338f0/charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262", size = 132630, upload-time = "2026-03-06T06:01:33.056Z" }, + { url = "https://files.pythonhosted.org/packages/70/53/e44a4c07e8904500aec95865dc3f6464dc3586a039ef0df606eb3ac38e35/charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636", size = 142856, upload-time = "2026-03-06T06:01:34.489Z" }, + { url = "https://files.pythonhosted.org/packages/ea/aa/c5628f7cad591b1cf45790b7a61483c3e36cf41349c98af7813c483fd6e8/charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02", size = 132982, upload-time = "2026-03-06T06:01:35.641Z" }, + { url = "https://files.pythonhosted.org/packages/f5/48/9f34ec4bb24aa3fdba1890c1bddb97c8a4be1bd84ef5c42ac2352563ad05/charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23", size = 280788, upload-time = "2026-03-06T06:01:37.126Z" }, + { url = "https://files.pythonhosted.org/packages/0e/09/6003e7ffeb90cc0560da893e3208396a44c210c5ee42efff539639def59b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8", size = 188890, upload-time = "2026-03-06T06:01:38.73Z" }, + { url = "https://files.pythonhosted.org/packages/42/1e/02706edf19e390680daa694d17e2b8eab4b5f7ac285e2a51168b4b22ee6b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d", size = 206136, upload-time = "2026-03-06T06:01:40.016Z" }, + { url = "https://files.pythonhosted.org/packages/c7/87/942c3def1b37baf3cf786bad01249190f3ca3d5e63a84f831e704977de1f/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce", size = 202551, upload-time = "2026-03-06T06:01:41.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/0a/af49691938dfe175d71b8a929bd7e4ace2809c0c5134e28bc535660d5262/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819", size = 195572, upload-time = "2026-03-06T06:01:43.208Z" }, + { url = "https://files.pythonhosted.org/packages/20/ea/dfb1792a8050a8e694cfbde1570ff97ff74e48afd874152d38163d1df9ae/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d", size = 184438, upload-time = "2026-03-06T06:01:44.755Z" }, + { url = "https://files.pythonhosted.org/packages/72/12/c281e2067466e3ddd0595bfaea58a6946765ace5c72dfa3edc2f5f118026/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763", size = 193035, upload-time = "2026-03-06T06:01:46.051Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4f/3792c056e7708e10464bad0438a44708886fb8f92e3c3d29ec5e2d964d42/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9", size = 191340, upload-time = "2026-03-06T06:01:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/e7/86/80ddba897127b5c7a9bccc481b0cd36c8fefa485d113262f0fe4332f0bf4/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c", size = 185464, upload-time = "2026-03-06T06:01:48.764Z" }, + { url = "https://files.pythonhosted.org/packages/4d/00/b5eff85ba198faacab83e0e4b6f0648155f072278e3b392a82478f8b988b/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67", size = 208014, upload-time = "2026-03-06T06:01:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/d36f70be01597fd30850dde8a1269ebc8efadd23ba5785808454f2389bde/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3", size = 193297, upload-time = "2026-03-06T06:01:51.933Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1d/259eb0a53d4910536c7c2abb9cb25f4153548efb42800c6a9456764649c0/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf", size = 204321, upload-time = "2026-03-06T06:01:53.887Z" }, + { url = "https://files.pythonhosted.org/packages/84/31/faa6c5b9d3688715e1ed1bb9d124c384fe2fc1633a409e503ffe1c6398c1/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6", size = 197509, upload-time = "2026-03-06T06:01:56.439Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a5/c7d9dd1503ffc08950b3260f5d39ec2366dd08254f0900ecbcf3a6197c7c/charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f", size = 132284, upload-time = "2026-03-06T06:01:57.812Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0f/57072b253af40c8aa6636e6de7d75985624c1eb392815b2f934199340a89/charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7", size = 142630, upload-time = "2026-03-06T06:01:59.062Z" }, + { url = "https://files.pythonhosted.org/packages/31/41/1c4b7cc9f13bd9d369ce3bc993e13d374ce25fa38a2663644283ecf422c1/charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36", size = 133254, upload-time = "2026-03-06T06:02:00.281Z" }, + { url = "https://files.pythonhosted.org/packages/43/be/0f0fd9bb4a7fa4fb5067fb7d9ac693d4e928d306f80a0d02bde43a7c4aee/charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873", size = 280232, upload-time = "2026-03-06T06:02:01.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/02/983b5445e4bef49cd8c9da73a8e029f0825f39b74a06d201bfaa2e55142a/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f", size = 189688, upload-time = "2026-03-06T06:02:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/d0/88/152745c5166437687028027dc080e2daed6fe11cfa95a22f4602591c42db/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4", size = 206833, upload-time = "2026-03-06T06:02:05.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0f/ebc15c8b02af2f19be9678d6eed115feeeccc45ce1f4b098d986c13e8769/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee", size = 202879, upload-time = "2026-03-06T06:02:06.446Z" }, + { url = "https://files.pythonhosted.org/packages/38/9c/71336bff6934418dc8d1e8a1644176ac9088068bc571da612767619c97b3/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66", size = 195764, upload-time = "2026-03-06T06:02:08.763Z" }, + { url = "https://files.pythonhosted.org/packages/b7/95/ce92fde4f98615661871bc282a856cf9b8a15f686ba0af012984660d480b/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362", size = 183728, upload-time = "2026-03-06T06:02:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e7/f5b4588d94e747ce45ae680f0f242bc2d98dbd4eccfab73e6160b6893893/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7", size = 192937, upload-time = "2026-03-06T06:02:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/f9/29/9d94ed6b929bf9f48bf6ede6e7474576499f07c4c5e878fb186083622716/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d", size = 192040, upload-time = "2026-03-06T06:02:13.489Z" }, + { url = "https://files.pythonhosted.org/packages/15/d2/1a093a1cf827957f9445f2fe7298bcc16f8fc5e05c1ed2ad1af0b239035e/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6", size = 184107, upload-time = "2026-03-06T06:02:14.83Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7d/82068ce16bd36135df7b97f6333c5d808b94e01d4599a682e2337ed5fd14/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39", size = 208310, upload-time = "2026-03-06T06:02:16.165Z" }, + { url = "https://files.pythonhosted.org/packages/84/4e/4dfb52307bb6af4a5c9e73e482d171b81d36f522b21ccd28a49656baa680/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6", size = 192918, upload-time = "2026-03-06T06:02:18.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/a4/159ff7da662cf7201502ca89980b8f06acf3e887b278956646a8aeb178ab/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94", size = 204615, upload-time = "2026-03-06T06:02:19.821Z" }, + { url = "https://files.pythonhosted.org/packages/d6/62/0dd6172203cb6b429ffffc9935001fde42e5250d57f07b0c28c6046deb6b/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e", size = 197784, upload-time = "2026-03-06T06:02:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5e/1aab5cb737039b9c59e63627dc8bbc0d02562a14f831cc450e5f91d84ce1/charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2", size = 133009, upload-time = "2026-03-06T06:02:23.289Z" }, + { url = "https://files.pythonhosted.org/packages/40/65/e7c6c77d7aaa4c0d7974f2e403e17f0ed2cb0fc135f77d686b916bf1eead/charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa", size = 143511, upload-time = "2026-03-06T06:02:26.195Z" }, + { url = "https://files.pythonhosted.org/packages/ba/91/52b0841c71f152f563b8e072896c14e3d83b195c188b338d3cc2e582d1d4/charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4", size = 133775, upload-time = "2026-03-06T06:02:27.473Z" }, + { url = "https://files.pythonhosted.org/packages/be/76/96dec962aa996081c48f544d5e9e97322006a1e67e8f76bad41f3fb0b151/charset_normalizer-3.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:259cd1ca995ad525f638e131dbcc2353a586564c038fc548a3fe450a91882139", size = 283220, upload-time = "2026-03-06T06:02:53.024Z" }, + { url = "https://files.pythonhosted.org/packages/cc/80/050c340587611be9743eff02d1ca34b5fc76a4356849dcb74dfd898d6d87/charset_normalizer-3.4.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a28afb04baa55abf26df544e3e5c6534245d3daa5178bc4a8eeb48202060d0e", size = 189988, upload-time = "2026-03-06T06:02:54.448Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a3/bb6caf9f5544ccaaca5c7e387fa868868d3420bcb03e8bc30f37be2e8a72/charset_normalizer-3.4.5-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ff95a9283de8a457e6b12989de3f9f5193430f375d64297d323a615ea52cbdb3", size = 207786, upload-time = "2026-03-06T06:02:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/ee/50/e56713141f2fdb3a4d46092425d58dc97a48e1e10ce321ac6ba43862aacf/charset_normalizer-3.4.5-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:708c7acde173eedd4bfa4028484426ba689d2103b28588c513b9db2cd5ecde9c", size = 203556, upload-time = "2026-03-06T06:02:57.31Z" }, + { url = "https://files.pythonhosted.org/packages/22/34/ed0cfd388dd9106725afc2beb036adbaa167fc0b5a9ee8cd3940757fb060/charset_normalizer-3.4.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa92ec1102eaff840ccd1021478af176a831f1bccb08e526ce844b7ddda85c22", size = 196552, upload-time = "2026-03-06T06:02:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8b/da4a4c3d26c539fdd777cfbd2c0d83e77e1218879517ef91c4ece7238563/charset_normalizer-3.4.5-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:5fea359734b140d0d6741189fea5478c6091b54ffc69d7ce119e0a05637d8c99", size = 184289, upload-time = "2026-03-06T06:03:00.448Z" }, + { url = "https://files.pythonhosted.org/packages/d3/05/9f67c1f94ea9ae1e08c8fa2182b1f5411732e18643e7080fc8c10ba1e021/charset_normalizer-3.4.5-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e545b51da9f9af5c67815ca0eb40676c0f016d0b0381c86f20451e35696c5f95", size = 195282, upload-time = "2026-03-06T06:03:02.161Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/aaf84a2e37e75470640e965d6619c6d9a521eb7c8aa097f2586907859198/charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:30987f4a8ed169983f93e1be8ffeea5214a779e27ed0b059835c7afe96550ad7", size = 192889, upload-time = "2026-03-06T06:03:03.629Z" }, + { url = "https://files.pythonhosted.org/packages/eb/94/9b714873baf9a841613e8b49a5a3cd77d985d2c6c80f5038a5057395ebac/charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:149ec69866c3d6c2fb6f758dbc014ecb09f30b35a5ca90b6a8a2d4e54e18fdfe", size = 185738, upload-time = "2026-03-06T06:03:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/ab/e5/bf57e1a9210a6ba78c740d66d05165a55b2cbeca29a83b8c659c9eb2d6c6/charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:530beedcec9b6e027e7a4b6ce26eed36678aa39e17da85e6e03d7bd9e8e9d7c9", size = 209458, upload-time = "2026-03-06T06:03:06.54Z" }, + { url = "https://files.pythonhosted.org/packages/65/91/3c8cb46d840840f2593028fd708ea50695f8f61e1c490530ef1cce824f56/charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:14498a429321de554b140013142abe7608f9d8ccc04d7baf2ad60498374aefa2", size = 195792, upload-time = "2026-03-06T06:03:08Z" }, + { url = "https://files.pythonhosted.org/packages/b0/43/783be5c6932fa8846a98313a2242fbcfe0c06c1c0ac2d6856b99d93069eb/charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2820a98460c83663dd8ec015d9ddfd1e4879f12e06bb7d0500f044fb477d2770", size = 204829, upload-time = "2026-03-06T06:03:09.488Z" }, + { url = "https://files.pythonhosted.org/packages/36/7d/138b5311c32fd24396321db796538cc748287c92da5e6fc1996babc06f99/charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:aa2f963b4da26daf46231d9b9e0e2c9408a751f8f0d0f44d2de56d3caf51d294", size = 198558, upload-time = "2026-03-06T06:03:11.585Z" }, + { url = "https://files.pythonhosted.org/packages/9c/87/ddd8bbdd703707c019fe9d14b678011627e6c5131dfdefe42aff151d718c/charset_normalizer-3.4.5-cp39-cp39-win32.whl", hash = "sha256:82cc7c2ad42faec8b574351f8bc2a0c049043893853317bd9bb309f5aba6cb5a", size = 132370, upload-time = "2026-03-06T06:03:13.327Z" }, + { url = "https://files.pythonhosted.org/packages/59/f6/d7cd28ae6d4dd47170b95153986789d69af4d5844f640edbc5138e4a70a2/charset_normalizer-3.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:92263f7eca2f4af326cd20de8d16728d2602f7cfea02e790dcde9d83c365d7cc", size = 142877, upload-time = "2026-03-06T06:03:15.041Z" }, + { url = "https://files.pythonhosted.org/packages/9c/26/8d68681566f288998eb36a0c60dd2c5c8aa93ee67b0d7e3dc72606650828/charset_normalizer-3.4.5-cp39-cp39-win_arm64.whl", hash = "sha256:014837af6fabf57121b6254fa8ade10dceabc3528b27b721a64bbc7b8b1d4eb4", size = 133186, upload-time = "2026-03-06T06:03:16.476Z" }, + { url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455, upload-time = "2026-03-06T06:03:17.827Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "comm" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/f6/31a8f28b4a2a4fa0e01085e542f3081ab0588eff8e589d39d775172c9792/contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4", size = 13464370, upload-time = "2024-08-27T21:00:03.328Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/e0/be8dcc796cfdd96708933e0e2da99ba4bb8f9b2caa9d560a50f3f09a65f3/contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7", size = 265366, upload-time = "2024-08-27T20:50:09.947Z" }, + { url = "https://files.pythonhosted.org/packages/50/d6/c953b400219443535d412fcbbc42e7a5e823291236bc0bb88936e3cc9317/contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42", size = 249226, upload-time = "2024-08-27T20:50:16.1Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b4/6fffdf213ffccc28483c524b9dad46bb78332851133b36ad354b856ddc7c/contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7", size = 308460, upload-time = "2024-08-27T20:50:22.536Z" }, + { url = "https://files.pythonhosted.org/packages/cf/6c/118fc917b4050f0afe07179a6dcbe4f3f4ec69b94f36c9e128c4af480fb8/contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab", size = 347623, upload-time = "2024-08-27T20:50:28.806Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a4/30ff110a81bfe3abf7b9673284d21ddce8cc1278f6f77393c91199da4c90/contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589", size = 317761, upload-time = "2024-08-27T20:50:35.126Z" }, + { url = "https://files.pythonhosted.org/packages/99/e6/d11966962b1aa515f5586d3907ad019f4b812c04e4546cc19ebf62b5178e/contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41", size = 322015, upload-time = "2024-08-27T20:50:40.318Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e3/182383743751d22b7b59c3c753277b6aee3637049197624f333dac5b4c80/contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d", size = 1262672, upload-time = "2024-08-27T20:50:55.643Z" }, + { url = "https://files.pythonhosted.org/packages/78/53/974400c815b2e605f252c8fb9297e2204347d1755a5374354ee77b1ea259/contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223", size = 1321688, upload-time = "2024-08-27T20:51:11.293Z" }, + { url = "https://files.pythonhosted.org/packages/52/29/99f849faed5593b2926a68a31882af98afbeac39c7fdf7de491d9c85ec6a/contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f", size = 171145, upload-time = "2024-08-27T20:51:15.2Z" }, + { url = "https://files.pythonhosted.org/packages/a9/97/3f89bba79ff6ff2b07a3cbc40aa693c360d5efa90d66e914f0ff03b95ec7/contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b", size = 216019, upload-time = "2024-08-27T20:51:19.365Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1f/9375917786cb39270b0ee6634536c0e22abf225825602688990d8f5c6c19/contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad", size = 266356, upload-time = "2024-08-27T20:51:24.146Z" }, + { url = "https://files.pythonhosted.org/packages/05/46/9256dd162ea52790c127cb58cfc3b9e3413a6e3478917d1f811d420772ec/contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49", size = 250915, upload-time = "2024-08-27T20:51:28.683Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5d/3056c167fa4486900dfbd7e26a2fdc2338dc58eee36d490a0ed3ddda5ded/contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66", size = 310443, upload-time = "2024-08-27T20:51:33.675Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c2/1a612e475492e07f11c8e267ea5ec1ce0d89971be496c195e27afa97e14a/contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081", size = 348548, upload-time = "2024-08-27T20:51:39.322Z" }, + { url = "https://files.pythonhosted.org/packages/45/cf/2c2fc6bb5874158277b4faf136847f0689e1b1a1f640a36d76d52e78907c/contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1", size = 319118, upload-time = "2024-08-27T20:51:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/03/33/003065374f38894cdf1040cef474ad0546368eea7e3a51d48b8a423961f8/contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d", size = 323162, upload-time = "2024-08-27T20:51:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/42/80/e637326e85e4105a802e42959f56cff2cd39a6b5ef68d5d9aee3ea5f0e4c/contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c", size = 1265396, upload-time = "2024-08-27T20:52:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3b/8cbd6416ca1bbc0202b50f9c13b2e0b922b64be888f9d9ee88e6cfabfb51/contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb", size = 1324297, upload-time = "2024-08-27T20:52:21.843Z" }, + { url = "https://files.pythonhosted.org/packages/4d/2c/021a7afaa52fe891f25535506cc861c30c3c4e5a1c1ce94215e04b293e72/contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c", size = 171808, upload-time = "2024-08-27T20:52:25.163Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/804f02ff30a7fae21f98198828d0857439ec4c91a96e20cf2d6c49372966/contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67", size = 217181, upload-time = "2024-08-27T20:52:29.13Z" }, + { url = "https://files.pythonhosted.org/packages/c9/92/8e0bbfe6b70c0e2d3d81272b58c98ac69ff1a4329f18c73bd64824d8b12e/contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f", size = 267838, upload-time = "2024-08-27T20:52:33.911Z" }, + { url = "https://files.pythonhosted.org/packages/e3/04/33351c5d5108460a8ce6d512307690b023f0cfcad5899499f5c83b9d63b1/contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6", size = 251549, upload-time = "2024-08-27T20:52:39.179Z" }, + { url = "https://files.pythonhosted.org/packages/51/3d/aa0fe6ae67e3ef9f178389e4caaaa68daf2f9024092aa3c6032e3d174670/contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639", size = 303177, upload-time = "2024-08-27T20:52:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/56/c3/c85a7e3e0cab635575d3b657f9535443a6f5d20fac1a1911eaa4bbe1aceb/contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c", size = 341735, upload-time = "2024-08-27T20:52:51.05Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/20f7a211a7be966a53f474bc90b1a8202e9844b3f1ef85f3ae45a77151ee/contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06", size = 314679, upload-time = "2024-08-27T20:52:58.473Z" }, + { url = "https://files.pythonhosted.org/packages/6e/be/524e377567defac0e21a46e2a529652d165fed130a0d8a863219303cee18/contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09", size = 320549, upload-time = "2024-08-27T20:53:06.593Z" }, + { url = "https://files.pythonhosted.org/packages/0f/96/fdb2552a172942d888915f3a6663812e9bc3d359d53dafd4289a0fb462f0/contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd", size = 1263068, upload-time = "2024-08-27T20:53:23.442Z" }, + { url = "https://files.pythonhosted.org/packages/2a/25/632eab595e3140adfa92f1322bf8915f68c932bac468e89eae9974cf1c00/contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35", size = 1322833, upload-time = "2024-08-27T20:53:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/69738782e315a1d26d29d71a550dbbe3eb6c653b028b150f70c1a5f4f229/contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb", size = 172681, upload-time = "2024-08-27T20:53:43.05Z" }, + { url = "https://files.pythonhosted.org/packages/0c/89/9830ba00d88e43d15e53d64931e66b8792b46eb25e2050a88fec4a0df3d5/contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b", size = 218283, upload-time = "2024-08-27T20:53:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/53/a1/d20415febfb2267af2d7f06338e82171824d08614084714fb2c1dac9901f/contourpy-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3e1c7fa44aaae40a2247e2e8e0627f4bea3dd257014764aa644f319a5f8600e3", size = 267879, upload-time = "2024-08-27T20:53:51.597Z" }, + { url = "https://files.pythonhosted.org/packages/aa/45/5a28a3570ff6218d8bdfc291a272a20d2648104815f01f0177d103d985e1/contourpy-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:364174c2a76057feef647c802652f00953b575723062560498dc7930fc9b1cb7", size = 251573, upload-time = "2024-08-27T20:53:55.659Z" }, + { url = "https://files.pythonhosted.org/packages/39/1c/d3f51540108e3affa84f095c8b04f0aa833bb797bc8baa218a952a98117d/contourpy-1.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32b238b3b3b649e09ce9aaf51f0c261d38644bdfa35cbaf7b263457850957a84", size = 303184, upload-time = "2024-08-27T20:54:00.225Z" }, + { url = "https://files.pythonhosted.org/packages/00/56/1348a44fb6c3a558c1a3a0cd23d329d604c99d81bf5a4b58c6b71aab328f/contourpy-1.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d51fca85f9f7ad0b65b4b9fe800406d0d77017d7270d31ec3fb1cc07358fdea0", size = 340262, upload-time = "2024-08-27T20:54:05.234Z" }, + { url = "https://files.pythonhosted.org/packages/2b/23/00d665ba67e1bb666152131da07e0f24c95c3632d7722caa97fb61470eca/contourpy-1.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:732896af21716b29ab3e988d4ce14bc5133733b85956316fb0c56355f398099b", size = 313806, upload-time = "2024-08-27T20:54:09.889Z" }, + { url = "https://files.pythonhosted.org/packages/5a/42/3cf40f7040bb8362aea19af9a5fb7b32ce420f645dd1590edcee2c657cd5/contourpy-1.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d73f659398a0904e125280836ae6f88ba9b178b2fed6884f3b1f95b989d2c8da", size = 319710, upload-time = "2024-08-27T20:54:14.536Z" }, + { url = "https://files.pythonhosted.org/packages/05/32/f3bfa3fc083b25e1a7ae09197f897476ee68e7386e10404bdf9aac7391f0/contourpy-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c6c7c2408b7048082932cf4e641fa3b8ca848259212f51c8c59c45aa7ac18f14", size = 1264107, upload-time = "2024-08-27T20:54:29.735Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1e/1019d34473a736664f2439542b890b2dc4c6245f5c0d8cdfc0ccc2cab80c/contourpy-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f317576606de89da6b7e0861cf6061f6146ead3528acabff9236458a6ba467f8", size = 1322458, upload-time = "2024-08-27T20:54:45.507Z" }, + { url = "https://files.pythonhosted.org/packages/22/85/4f8bfd83972cf8909a4d36d16b177f7b8bdd942178ea4bf877d4a380a91c/contourpy-1.3.0-cp313-cp313-win32.whl", hash = "sha256:31cd3a85dbdf1fc002280c65caa7e2b5f65e4a973fcdf70dd2fdcb9868069294", size = 172643, upload-time = "2024-08-27T20:55:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4a/fb3c83c1baba64ba90443626c228ca14f19a87c51975d3b1de308dd2cf08/contourpy-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4553c421929ec95fb07b3aaca0fae668b2eb5a5203d1217ca7c34c063c53d087", size = 218301, upload-time = "2024-08-27T20:55:56.509Z" }, + { url = "https://files.pythonhosted.org/packages/76/65/702f4064f397821fea0cb493f7d3bc95a5d703e20954dce7d6d39bacf378/contourpy-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:345af746d7766821d05d72cb8f3845dfd08dd137101a2cb9b24de277d716def8", size = 278972, upload-time = "2024-08-27T20:54:50.347Z" }, + { url = "https://files.pythonhosted.org/packages/80/85/21f5bba56dba75c10a45ec00ad3b8190dbac7fd9a8a8c46c6116c933e9cf/contourpy-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3bb3808858a9dc68f6f03d319acd5f1b8a337e6cdda197f02f4b8ff67ad2057b", size = 263375, upload-time = "2024-08-27T20:54:54.909Z" }, + { url = "https://files.pythonhosted.org/packages/0a/64/084c86ab71d43149f91ab3a4054ccf18565f0a8af36abfa92b1467813ed6/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:420d39daa61aab1221567b42eecb01112908b2cab7f1b4106a52caaec8d36973", size = 307188, upload-time = "2024-08-27T20:55:00.184Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/d61a4c288dc42da0084b8d9dc2aa219a850767165d7d9a9c364ff530b509/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d63ee447261e963af02642ffcb864e5a2ee4cbfd78080657a9880b8b1868e18", size = 345644, upload-time = "2024-08-27T20:55:05.673Z" }, + { url = "https://files.pythonhosted.org/packages/ca/aa/00d2313d35ec03f188e8f0786c2fc61f589306e02fdc158233697546fd58/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:167d6c890815e1dac9536dca00828b445d5d0df4d6a8c6adb4a7ec3166812fa8", size = 317141, upload-time = "2024-08-27T20:55:11.047Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6a/b5242c8cb32d87f6abf4f5e3044ca397cb1a76712e3fa2424772e3ff495f/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:710a26b3dc80c0e4febf04555de66f5fd17e9cf7170a7b08000601a10570bda6", size = 323469, upload-time = "2024-08-27T20:55:15.914Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a6/73e929d43028a9079aca4bde107494864d54f0d72d9db508a51ff0878593/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:75ee7cb1a14c617f34a51d11fa7524173e56551646828353c4af859c56b766e2", size = 1260894, upload-time = "2024-08-27T20:55:31.553Z" }, + { url = "https://files.pythonhosted.org/packages/2b/1e/1e726ba66eddf21c940821df8cf1a7d15cb165f0682d62161eaa5e93dae1/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:33c92cdae89ec5135d036e7218e69b0bb2851206077251f04a6c4e0e21f03927", size = 1314829, upload-time = "2024-08-27T20:55:47.837Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/b9f72758adb6ef7397327ceb8b9c39c75711affb220e4f53c745ea1d5a9a/contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8", size = 265518, upload-time = "2024-08-27T20:56:01.333Z" }, + { url = "https://files.pythonhosted.org/packages/ec/22/19f5b948367ab5260fb41d842c7a78dae645603881ea6bc39738bcfcabf6/contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c", size = 249350, upload-time = "2024-08-27T20:56:05.432Z" }, + { url = "https://files.pythonhosted.org/packages/26/76/0c7d43263dd00ae21a91a24381b7e813d286a3294d95d179ef3a7b9fb1d7/contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca", size = 309167, upload-time = "2024-08-27T20:56:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/96/3b/cadff6773e89f2a5a492c1a8068e21d3fccaf1a1c1df7d65e7c8e3ef60ba/contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f", size = 348279, upload-time = "2024-08-27T20:56:15.41Z" }, + { url = "https://files.pythonhosted.org/packages/e1/86/158cc43aa549d2081a955ab11c6bdccc7a22caacc2af93186d26f5f48746/contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc", size = 318519, upload-time = "2024-08-27T20:56:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/05/11/57335544a3027e9b96a05948c32e566328e3a2f84b7b99a325b7a06d2b06/contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2", size = 321922, upload-time = "2024-08-27T20:56:26.983Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e3/02114f96543f4a1b694333b92a6dcd4f8eebbefcc3a5f3bbb1316634178f/contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e", size = 1258017, upload-time = "2024-08-27T20:56:42.246Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3b/bfe4c81c6d5881c1c643dde6620be0b42bf8aab155976dd644595cfab95c/contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800", size = 1316773, upload-time = "2024-08-27T20:56:58.58Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/c52d2970784383cafb0bd918b6fb036d98d96bbf0bc1befb5d1e31a07a70/contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5", size = 171353, upload-time = "2024-08-27T20:57:02.718Z" }, + { url = "https://files.pythonhosted.org/packages/53/23/db9f69676308e094d3c45f20cc52e12d10d64f027541c995d89c11ad5c75/contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843", size = 211817, upload-time = "2024-08-27T20:57:06.328Z" }, + { url = "https://files.pythonhosted.org/packages/d1/09/60e486dc2b64c94ed33e58dcfb6f808192c03dfc5574c016218b9b7680dc/contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c", size = 261886, upload-time = "2024-08-27T20:57:10.863Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/b57f9f7174fcd439a7789fb47d764974ab646fa34d1790551de386457a8e/contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779", size = 311008, upload-time = "2024-08-27T20:57:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/74/fc/5040d42623a1845d4f17a418e590fd7a79ae8cb2bad2b2f83de63c3bdca4/contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4", size = 215690, upload-time = "2024-08-27T20:57:19.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/dc3dcd77ac7460ab7e9d2b01a618cb31406902e50e605a8d6091f0a8f7cc/contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0", size = 261894, upload-time = "2024-08-27T20:57:23.873Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/531642a01cfec39d1682e46b5457b07cf805e3c3c584ec27e2a6223f8f6c/contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102", size = 311099, upload-time = "2024-08-27T20:57:28.58Z" }, + { url = "https://files.pythonhosted.org/packages/38/1e/94bda024d629f254143a134eead69e21c836429a2a6ce82209a00ddcb79a/contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb", size = 215838, upload-time = "2024-08-27T20:57:32.913Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/be/8bd693a0b9d53d48c8978fa5d889e06f3b5b03e45fd1ea1e78267b4887cb/debugpy-1.8.20-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:157e96ffb7f80b3ad36d808646198c90acb46fdcfd8bb1999838f0b6f2b59c64", size = 2099192, upload-time = "2026-01-29T23:03:29.707Z" }, + { url = "https://files.pythonhosted.org/packages/77/1b/85326d07432086a06361d493d2743edd0c4fc2ef62162be7f8618441ac37/debugpy-1.8.20-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:c1178ae571aff42e61801a38b007af504ec8e05fde1c5c12e5a7efef21009642", size = 3088568, upload-time = "2026-01-29T23:03:31.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/60/3e08462ee3eccd10998853eb35947c416e446bfe2bc37dbb886b9044586c/debugpy-1.8.20-cp310-cp310-win32.whl", hash = "sha256:c29dd9d656c0fbd77906a6e6a82ae4881514aa3294b94c903ff99303e789b4a2", size = 5284399, upload-time = "2026-01-29T23:03:33.678Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/09d49106e770fe558ced5e80df2e3c2ebee10e576eda155dcc5670473663/debugpy-1.8.20-cp310-cp310-win_amd64.whl", hash = "sha256:3ca85463f63b5dd0aa7aaa933d97cbc47c174896dcae8431695872969f981893", size = 5316388, upload-time = "2026-01-29T23:03:35.095Z" }, + { url = "https://files.pythonhosted.org/packages/51/56/c3baf5cbe4dd77427fd9aef99fcdade259ad128feeb8a786c246adb838e5/debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b", size = 2208318, upload-time = "2026-01-29T23:03:36.481Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7d/4fa79a57a8e69fe0d9763e98d1110320f9ecd7f1f362572e3aafd7417c9d/debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344", size = 3171493, upload-time = "2026-01-29T23:03:37.775Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/1e8f8affe51e12a26f3a8a8a4277d6e60aa89d0a66512f63b1e799d424a4/debugpy-1.8.20-cp311-cp311-win32.whl", hash = "sha256:773e839380cf459caf73cc533ea45ec2737a5cc184cf1b3b796cd4fd98504fec", size = 5209240, upload-time = "2026-01-29T23:03:39.109Z" }, + { url = "https://files.pythonhosted.org/packages/d5/92/1cb532e88560cbee973396254b21bece8c5d7c2ece958a67afa08c9f10dc/debugpy-1.8.20-cp311-cp311-win_amd64.whl", hash = "sha256:1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb", size = 5233481, upload-time = "2026-01-29T23:03:40.659Z" }, + { url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686, upload-time = "2026-01-29T23:03:42.023Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" }, + { url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372, upload-time = "2026-01-29T23:03:45.526Z" }, + { url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835, upload-time = "2026-01-29T23:03:47.245Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560, upload-time = "2026-01-29T23:03:48.738Z" }, + { url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272, upload-time = "2026-01-29T23:03:50.169Z" }, + { url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208, upload-time = "2026-01-29T23:03:51.547Z" }, + { url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930, upload-time = "2026-01-29T23:03:53.585Z" }, + { url = "https://files.pythonhosted.org/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173", size = 2538066, upload-time = "2026-01-29T23:03:54.999Z" }, + { url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425, upload-time = "2026-01-29T23:03:56.518Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407, upload-time = "2026-01-29T23:03:58.481Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521, upload-time = "2026-01-29T23:03:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6b/668f21567e3250463beb6a401e7d598baa2a0907224000d7f68b9442c243/debugpy-1.8.20-cp39-cp39-macosx_15_0_x86_64.whl", hash = "sha256:bff8990f040dacb4c314864da95f7168c5a58a30a66e0eea0fb85e2586a92cd6", size = 2100484, upload-time = "2026-01-29T23:04:09.929Z" }, + { url = "https://files.pythonhosted.org/packages/cf/49/223143d1da586b891f35a45515f152742ad85bfc10d2e02e697f65c83b32/debugpy-1.8.20-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:70ad9ae09b98ac307b82c16c151d27ee9d68ae007a2e7843ba621b5ce65333b5", size = 3081272, upload-time = "2026-01-29T23:04:11.664Z" }, + { url = "https://files.pythonhosted.org/packages/b1/24/9f219c9290fe8bee4f63f9af8ebac440c802e6181d7f39a79abcb5fdff2f/debugpy-1.8.20-cp39-cp39-win32.whl", hash = "sha256:9eeed9f953f9a23850c85d440bf51e3c56ed5d25f8560eeb29add815bd32f7ee", size = 5285196, upload-time = "2026-01-29T23:04:13.105Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f3/4a12d7b1b09e3b79ba6e3edfa0c677b8b8bdf110bc4b3607e0f29fb4e8b3/debugpy-1.8.20-cp39-cp39-win_amd64.whl", hash = "sha256:760813b4fff517c75bfe7923033c107104e76acfef7bda011ffea8736e9a66f8", size = 5317163, upload-time = "2026-01-29T23:04:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "fast-forward-football" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "kloppy", marker = "python_full_version == '3.11.*'" }, + { name = "polars", marker = "python_full_version == '3.11.*'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/e1/157d8c3845fb6239e6bb323b4b0062b695ecb9746ed56062ed2625ada3ba/fast_forward_football-0.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:109e69d6eacb8d844a37d45a8ccf5903654340fdf7d6dac2c354fce3f537c98f", size = 7197771, upload-time = "2026-02-26T08:22:49.127Z" }, + { url = "https://files.pythonhosted.org/packages/be/97/a4eeda701391eed637922a14a27f7d3d99e2052070d033da3dd576e24535/fast_forward_football-0.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f840accebfac65237755100ce0dd34a1dbdb4d26bc8d7cb4c6ae2766a6b2e63", size = 6629103, upload-time = "2026-02-26T08:22:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/a1/af/c0751a6f2b95fbc114d0fe1733bb795d6c2032f2e7e965de42a1dec39290/fast_forward_football-0.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82e5f5b8d8973df894d272e6062a6a01090d87914f264e888c7a05b985b44ace", size = 7423194, upload-time = "2026-02-26T08:22:54.308Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2b/3cad264eb52304e4c5a898566f892d02019f5d141ec6da970c956aaa5e41/fast_forward_football-0.1.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:743490b3cff6bbe70107bbec064e48786c468958d2db10dbec721ec316d46e6e", size = 6316701, upload-time = "2026-02-26T08:22:56.573Z" }, + { url = "https://files.pythonhosted.org/packages/d2/a8/3278e57ae6877d1d390069db43f96f8ae36feca4f522f4f2219643c75da2/fast_forward_football-0.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:2de41b9548a0080b7604225c0aeaf45653c64912128bc247b205be2a6769fb13", size = 7449318, upload-time = "2026-02-26T08:23:00.234Z" }, + { url = "https://files.pythonhosted.org/packages/e7/74/7807b18b2c4e020bce01f7ab61b3bcaa1c8b29b059d649b583ad8f5db377/fast_forward_football-0.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:99b8330633f8cab625a244e3af021002c30123632b6718b4ef970ad05f5057f2", size = 7228402, upload-time = "2026-02-26T08:23:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/80/e1/505a2990dea6a47fe68ef873922e68dcdf302f273590dc2f6c2dc9c91a73/fast_forward_football-0.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:632314ef1ab8768e0ea6ec10bcc4e06579bdfaec88340facf4dbd16a60efae9b", size = 6621155, upload-time = "2026-02-26T08:23:05.343Z" }, + { url = "https://files.pythonhosted.org/packages/76/e9/97f6270545abe6928184c58220f5bd103968a6834dddb1da7c1b1df112d8/fast_forward_football-0.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1ce584f8ff47c6e9f551e7e15f04ae028243f3770208820a25622abfe5f20b1", size = 7416055, upload-time = "2026-02-26T08:23:07.429Z" }, + { url = "https://files.pythonhosted.org/packages/19/a6/45a2e311a8d31ebf4c2caa1c20886ebdf0b11cbd7ae54f3f9246d3d2ccc5/fast_forward_football-0.1.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8d9562fd39553e9fb93fb7eacf4a3f23a86158896764c59c74842b58ada33f1c", size = 6314738, upload-time = "2026-02-26T08:23:09.895Z" }, + { url = "https://files.pythonhosted.org/packages/e7/02/cfb8ca068f1cadda2cf826d241a522c06ce28f5dd429fb0578004a3e9cb4/fast_forward_football-0.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:188a9c3bfbdd2ad3850431b2bd2ed3cf2cb9ddbbb88e407b5bd3e6830284b1a9", size = 7442152, upload-time = "2026-02-26T08:23:12.174Z" }, + { url = "https://files.pythonhosted.org/packages/a2/4d/359b3102472f676908d2fa20db0d038c7ffc178e560d7fac032c10993042/fast_forward_football-0.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:4d87517dc3741287aeb5ed80860577bd1cca7295853a870fe7093fa7f1fcdd23", size = 7227906, upload-time = "2026-02-26T08:23:14.837Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/8a238fbcd2604eccf1c9cf121f911c9f5e21f401befe5d15f732297f0207/fast_forward_football-0.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30a67414b61a086c3387858e3e3ac64bce4e9e154e0e5c262d56d0c8ea8908f7", size = 6621009, upload-time = "2026-02-26T08:23:17.757Z" }, + { url = "https://files.pythonhosted.org/packages/e5/82/29cb557af2ab57c9900b1d535c66707dae604ba42492060e6cffc07b5925/fast_forward_football-0.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b1ecf259886fd455961960366ae4aa7b35349d34eff938baedb5cb9847a95f0", size = 7415125, upload-time = "2026-02-26T08:23:19.804Z" }, + { url = "https://files.pythonhosted.org/packages/fe/b8/05d73f0c74de201716e3f0bb7e8fbf7853b11b27df2e9b055a6a30aac196/fast_forward_football-0.1.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:cdf629decf6b8539ee7c0b620e92cc771427ac5aee69f50944592d6c8f99ac77", size = 6313550, upload-time = "2026-02-26T08:23:22.155Z" }, + { url = "https://files.pythonhosted.org/packages/ca/28/70681125529ca8f7b629120b6be5be4f53aab42a6b939b8a5c139ace7441/fast_forward_football-0.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:2bc2431588f238a7a4d9c25b609a6631d81b9f83d1f83883329cc7eed7b609cd", size = 7441015, upload-time = "2026-02-26T08:23:24.371Z" }, +] + +[[package]] +name = "filelock" +version = "3.19.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, +] + +[[package]] +name = "filelock" +version = "3.25.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/77/18/a1fd2231c679dcb9726204645721b12498aeac28e1ad0601038f94b42556/filelock-3.25.0.tar.gz", hash = "sha256:8f00faf3abf9dc730a1ffe9c354ae5c04e079ab7d3a683b7c32da5dd05f26af3", size = 40158, upload-time = "2026-03-01T15:08:45.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/0b/de6f54d4a8bedfe8645c41497f3c18d749f0bd3218170c667bf4b81d0cdd/filelock-3.25.0-py3-none-any.whl", hash = "sha256:5ccf8069f7948f494968fc0713c10e5c182a9c9d9eef3a636307a20c2490f047", size = 26427, upload-time = "2026-03-01T15:08:44.593Z" }, +] + +[[package]] +name = "fonttools" +version = "4.60.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/c4/db6a7b5eb0656534c3aa2596c2c5e18830d74f1b9aa5aa8a7dff63a0b11d/fonttools-4.60.2.tar.gz", hash = "sha256:d29552e6b155ebfc685b0aecf8d429cb76c14ab734c22ef5d3dea6fdf800c92c", size = 3562254, upload-time = "2025-12-09T13:38:11.835Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/de/9e10a99fb3070accb8884886a41a4ce54e49bf2fa4fc63f48a6cf2061713/fonttools-4.60.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e36fadcf7e8ca6e34d490eef86ed638d6fd9c55d2f514b05687622cfc4a7050", size = 2850403, upload-time = "2025-12-09T13:35:53.14Z" }, + { url = "https://files.pythonhosted.org/packages/e4/40/d5b369d1073b134f600a94a287e13b5bdea2191ba6347d813fa3da00e94a/fonttools-4.60.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e500fc9c04bee749ceabfc20cb4903f6981c2139050d85720ea7ada61b75d5c", size = 2398629, upload-time = "2025-12-09T13:35:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b5/123819369aaf99d1e4dc49f1de1925d4edc7379114d15a56a7dd2e9d56e6/fonttools-4.60.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22efea5e784e1d1cd8d7b856c198e360a979383ebc6dea4604743b56da1cbc34", size = 4893471, upload-time = "2025-12-09T13:35:58.927Z" }, + { url = "https://files.pythonhosted.org/packages/24/29/f8f8acccb9716b899be4be45e9ce770d6aa76327573863e68448183091b0/fonttools-4.60.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:677aa92d84d335e4d301d8ba04afca6f575316bc647b6782cb0921943fcb6343", size = 4854686, upload-time = "2025-12-09T13:36:01.767Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0d/f3f51d7519f44f2dd5c9a60d7cd41185ebcee4348f073e515a3a93af15ff/fonttools-4.60.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:edd49d3defbf35476e78b61ff737ff5efea811acff68d44233a95a5a48252334", size = 4871233, upload-time = "2025-12-09T13:36:06.094Z" }, + { url = "https://files.pythonhosted.org/packages/cc/3f/4d4fd47d3bc40ab4d76718555185f8adffb5602ea572eac4bbf200c47d22/fonttools-4.60.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:126839492b69cecc5baf2bddcde60caab2ffafd867bbae2a88463fce6078ca3a", size = 4988936, upload-time = "2025-12-09T13:36:08.42Z" }, + { url = "https://files.pythonhosted.org/packages/01/6f/83bbdefa43f2c3ae206fd8c4b9a481f3c913eef871b1ce9a453069239e39/fonttools-4.60.2-cp310-cp310-win32.whl", hash = "sha256:ffcab6f5537136046ca902ed2491ab081ba271b07591b916289b7c27ff845f96", size = 2278044, upload-time = "2025-12-09T13:36:10.641Z" }, + { url = "https://files.pythonhosted.org/packages/d4/04/7d9a137e919d6c9ef26704b7f7b2580d9cfc5139597588227aacebc0e3b7/fonttools-4.60.2-cp310-cp310-win_amd64.whl", hash = "sha256:9c68b287c7ffcd29dd83b5f961004b2a54a862a88825d52ea219c6220309ba45", size = 2326522, upload-time = "2025-12-09T13:36:12.981Z" }, + { url = "https://files.pythonhosted.org/packages/e0/80/b7693d37c02417e162cc83cdd0b19a4f58be82c638b5d4ce4de2dae050c4/fonttools-4.60.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a2aed0a7931401b3875265717a24c726f87ecfedbb7b3426c2ca4d2812e281ae", size = 2847809, upload-time = "2025-12-09T13:36:14.884Z" }, + { url = "https://files.pythonhosted.org/packages/f9/9a/9c2c13bf8a6496ac21607d704e74e9cc68ebf23892cf924c9a8b5c7566b9/fonttools-4.60.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dea6868e9d2b816c9076cfea77754686f3c19149873bdbc5acde437631c15df1", size = 2397302, upload-time = "2025-12-09T13:36:17.151Z" }, + { url = "https://files.pythonhosted.org/packages/56/f6/ce38ff6b2d2d58f6fd981d32f3942365bfa30eadf2b47d93b2d48bf6097f/fonttools-4.60.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fa27f34950aa1fe0f0b1abe25eed04770a3b3b34ad94e5ace82cc341589678a", size = 5054418, upload-time = "2025-12-09T13:36:19.062Z" }, + { url = "https://files.pythonhosted.org/packages/88/06/5353bea128ff39e857c31de3dd605725b4add956badae0b31bc9a50d4c8e/fonttools-4.60.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:13a53d479d187b09bfaa4a35ffcbc334fc494ff355f0a587386099cb66674f1e", size = 5031652, upload-time = "2025-12-09T13:36:21.206Z" }, + { url = "https://files.pythonhosted.org/packages/71/05/ebca836437f6ebd57edd6428e7eff584e683ff0556ddb17d62e3b731f46c/fonttools-4.60.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fac5e921d3bd0ca3bb8517dced2784f0742bc8ca28579a68b139f04ea323a779", size = 5030321, upload-time = "2025-12-09T13:36:23.515Z" }, + { url = "https://files.pythonhosted.org/packages/57/f9/eb9d2a2ce30c99f840c1cc3940729a970923cf39d770caf88909d98d516b/fonttools-4.60.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:648f4f9186fd7f1f3cd57dbf00d67a583720d5011feca67a5e88b3a491952cfb", size = 5154255, upload-time = "2025-12-09T13:36:25.879Z" }, + { url = "https://files.pythonhosted.org/packages/08/a2/088b6ceba8272a9abb629d3c08f9c1e35e5ce42db0ccfe0c1f9f03e60d1d/fonttools-4.60.2-cp311-cp311-win32.whl", hash = "sha256:3274e15fad871bead5453d5ce02658f6d0c7bc7e7021e2a5b8b04e2f9e40da1a", size = 2276300, upload-time = "2025-12-09T13:36:27.772Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/8e4c3d908cc5dade7bb1316ce48589f6a24460c1056fd4b8db51f1fa309a/fonttools-4.60.2-cp311-cp311-win_amd64.whl", hash = "sha256:91d058d5a483a1525b367803abb69de0923fbd45e1f82ebd000f5c8aa65bc78e", size = 2327574, upload-time = "2025-12-09T13:36:30.89Z" }, + { url = "https://files.pythonhosted.org/packages/c0/30/530c9eddcd1c39219dc0aaede2b5a4c8ab80e0bb88d1b3ffc12944c4aac3/fonttools-4.60.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e0164b7609d2b5c5dd4e044b8085b7bd7ca7363ef8c269a4ab5b5d4885a426b2", size = 2847196, upload-time = "2025-12-09T13:36:33.262Z" }, + { url = "https://files.pythonhosted.org/packages/19/2f/4077a482836d5bbe3bc9dac1c004d02ee227cf04ed62b0a2dfc41d4f0dfd/fonttools-4.60.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1dd3d9574fc595c1e97faccae0f264dc88784ddf7fbf54c939528378bacc0033", size = 2395842, upload-time = "2025-12-09T13:36:35.47Z" }, + { url = "https://files.pythonhosted.org/packages/dd/05/aae5bb99c5398f8ed4a8b784f023fd9dd3568f0bd5d5b21e35b282550f11/fonttools-4.60.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98d0719f1b11c2817307d2da2e94296a3b2a3503f8d6252a101dca3ee663b917", size = 4949713, upload-time = "2025-12-09T13:36:37.874Z" }, + { url = "https://files.pythonhosted.org/packages/b4/37/49067349fc78ff0efbf09fadefe80ddf41473ca8f8a25400e3770da38328/fonttools-4.60.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d3ea26957dd07209f207b4fff64c702efe5496de153a54d3b91007ec28904dd", size = 4999907, upload-time = "2025-12-09T13:36:39.853Z" }, + { url = "https://files.pythonhosted.org/packages/16/31/d0f11c758bd0db36b664c92a0f9dfdcc2d7313749aa7d6629805c6946f21/fonttools-4.60.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ee301273b0850f3a515299f212898f37421f42ff9adfc341702582ca5073c13", size = 4939717, upload-time = "2025-12-09T13:36:43.075Z" }, + { url = "https://files.pythonhosted.org/packages/d9/bc/1cff0d69522e561bf1b99bee7c3911c08c25e919584827c3454a64651ce9/fonttools-4.60.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6eb4694cc3b9c03b7c01d65a9cf35b577f21aa6abdbeeb08d3114b842a58153", size = 5089205, upload-time = "2025-12-09T13:36:45.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e6/fb174f0069b7122e19828c551298bfd34fdf9480535d2a6ac2ed37afacd3/fonttools-4.60.2-cp312-cp312-win32.whl", hash = "sha256:57f07b616c69c244cc1a5a51072eeef07dddda5ebef9ca5c6e9cf6d59ae65b70", size = 2264674, upload-time = "2025-12-09T13:36:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/75/57/6552ffd6b582d3e6a9f01780c5275e6dfff1e70ca146101733aa1c12a129/fonttools-4.60.2-cp312-cp312-win_amd64.whl", hash = "sha256:310035802392f1fe5a7cf43d76f6ff4a24c919e4c72c0352e7b8176e2584b8a0", size = 2314701, upload-time = "2025-12-09T13:36:51.09Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e4/8381d0ca6b6c6c484660b03517ec5b5b81feeefca3808726dece36c652a9/fonttools-4.60.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2bb5fd231e56ccd7403212636dcccffc96c5ae0d6f9e4721fa0a32cb2e3ca432", size = 2842063, upload-time = "2025-12-09T13:36:53.468Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2c/4367117ee8ff4f4374787a1222da0bd413d80cf3522111f727a7b8f80d1d/fonttools-4.60.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:536b5fab7b6fec78ccf59b5c59489189d9d0a8b0d3a77ed1858be59afb096696", size = 2393792, upload-time = "2025-12-09T13:36:55.742Z" }, + { url = "https://files.pythonhosted.org/packages/49/b7/a76b6dffa193869e54e32ca2f9abb0d0e66784bc8a24e6f86eb093015481/fonttools-4.60.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b9288fc38252ac86a9570f19313ecbc9ff678982e0f27c757a85f1f284d3400", size = 4924020, upload-time = "2025-12-09T13:36:58.229Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4e/0078200e2259f0061c86a74075f507d64c43dd2ab38971956a5c0012d344/fonttools-4.60.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93fcb420791d839ef592eada2b69997c445d0ce9c969b5190f2e16828ec10607", size = 4980070, upload-time = "2025-12-09T13:37:00.311Z" }, + { url = "https://files.pythonhosted.org/packages/85/1f/d87c85a11cb84852c975251581862681e4a0c1c3bd456c648792203f311b/fonttools-4.60.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7916a381b094db4052ac284255186aebf74c5440248b78860cb41e300036f598", size = 4921411, upload-time = "2025-12-09T13:37:02.345Z" }, + { url = "https://files.pythonhosted.org/packages/75/c0/7efad650f5ed8e317c2633133ef3c64917e7adf2e4e2940c798f5d57ec6e/fonttools-4.60.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58c8c393d5e16b15662cfc2d988491940458aa87894c662154f50c7b49440bef", size = 5063465, upload-time = "2025-12-09T13:37:04.836Z" }, + { url = "https://files.pythonhosted.org/packages/18/a8/750518c4f8cdd79393b386bc81226047ade80239e58c6c9f5dbe1fdd8ea1/fonttools-4.60.2-cp313-cp313-win32.whl", hash = "sha256:19c6e0afd8b02008caa0aa08ab896dfce5d0bcb510c49b2c499541d5cb95a963", size = 2263443, upload-time = "2025-12-09T13:37:06.762Z" }, + { url = "https://files.pythonhosted.org/packages/b8/22/026c60376f165981f80a0e90bd98a79ae3334e9d89a3d046c4d2e265c724/fonttools-4.60.2-cp313-cp313-win_amd64.whl", hash = "sha256:6a500dc59e11b2338c2dba1f8cf11a4ae8be35ec24af8b2628b8759a61457b76", size = 2313800, upload-time = "2025-12-09T13:37:08.713Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ab/7cf1f5204e1366ddf9dc5cdc2789b571feb9eebcee0e3463c3f457df5f52/fonttools-4.60.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9387c532acbe323bbf2a920f132bce3c408a609d5f9dcfc6532fbc7e37f8ccbb", size = 2841690, upload-time = "2025-12-09T13:37:10.696Z" }, + { url = "https://files.pythonhosted.org/packages/00/3c/0bf83c6f863cc8b934952567fa2bf737cfcec8fc4ffb59b3f93820095f89/fonttools-4.60.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6f1c824185b5b8fb681297f315f26ae55abb0d560c2579242feea8236b1cfef", size = 2392191, upload-time = "2025-12-09T13:37:12.954Z" }, + { url = "https://files.pythonhosted.org/packages/00/f0/40090d148b8907fbea12e9bdf1ff149f30cdf1769e3b2c3e0dbf5106b88d/fonttools-4.60.2-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:55a3129d1e4030b1a30260f1b32fe76781b585fb2111d04a988e141c09eb6403", size = 4873503, upload-time = "2025-12-09T13:37:15.142Z" }, + { url = "https://files.pythonhosted.org/packages/dc/e0/d8b13f99e58b8c293781288ba62fe634f1f0697c9c4c0ae104d3215f3a10/fonttools-4.60.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b196e63753abc33b3b97a6fd6de4b7c4fef5552c0a5ba5e562be214d1e9668e0", size = 4968493, upload-time = "2025-12-09T13:37:18.272Z" }, + { url = "https://files.pythonhosted.org/packages/46/c5/960764d12c92bc225f02401d3067048cb7b282293d9e48e39fe2b0ec38a9/fonttools-4.60.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:de76c8d740fb55745f3b154f0470c56db92ae3be27af8ad6c2e88f1458260c9a", size = 4920015, upload-time = "2025-12-09T13:37:20.334Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ab/839d8caf253d1eef3653ef4d34427d0326d17a53efaec9eb04056b670fff/fonttools-4.60.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ba6303225c95998c9fda2d410aa792c3d2c1390a09df58d194b03e17583fa25", size = 5031165, upload-time = "2025-12-09T13:37:23.57Z" }, + { url = "https://files.pythonhosted.org/packages/de/bf/3bc862796a6841cbe0725bb5512d272239b809dba631a4b0301df885e62d/fonttools-4.60.2-cp314-cp314-win32.whl", hash = "sha256:0a89728ce10d7c816fedaa5380c06d2793e7a8a634d7ce16810e536c22047384", size = 2267526, upload-time = "2025-12-09T13:37:25.821Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a1/c1909cacf00c76dc37b4743451561fbaaf7db4172c22a6d9394081d114c3/fonttools-4.60.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa8446e6ab8bd778b82cb1077058a2addba86f30de27ab9cc18ed32b34bc8667", size = 2319096, upload-time = "2025-12-09T13:37:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/29/b3/f66e71433f08e3a931b2b31a665aeed17fcc5e6911fc73529c70a232e421/fonttools-4.60.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4063bc81ac5a4137642865cb63dd270e37b3cd1f55a07c0d6e41d072699ccca2", size = 2925167, upload-time = "2025-12-09T13:37:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/2e/13/eeb491ff743594bbd0bee6e49422c03a59fe9c49002d3cc60eeb77414285/fonttools-4.60.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebfdb66fa69732ed604ab8e2a0431e6deff35e933a11d73418cbc7823d03b8e1", size = 2430923, upload-time = "2025-12-09T13:37:32.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/db609f785e460796e53c4dbc3874a5f4948477f27beceb5e2d24b2537666/fonttools-4.60.2-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50b10b3b1a72d1d54c61b0e59239e1a94c0958f4a06a1febf97ce75388dd91a4", size = 4877729, upload-time = "2025-12-09T13:37:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d6/85e4484dd4bfb03fee7bd370d65888cccbd3dee2681ee48c869dd5ccb23f/fonttools-4.60.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:beae16891a13b4a2ddec9b39b4de76092a3025e4d1c82362e3042b62295d5e4d", size = 5096003, upload-time = "2025-12-09T13:37:37.862Z" }, + { url = "https://files.pythonhosted.org/packages/30/49/1a98e44b71030b83d2046f981373b80571868259d98e6dae7bc20099dac6/fonttools-4.60.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:522f017fdb3766fd5d2d321774ef351cc6ce88ad4e6ac9efe643e4a2b9d528db", size = 4974410, upload-time = "2025-12-09T13:37:40.166Z" }, + { url = "https://files.pythonhosted.org/packages/42/07/d6f775d950ee8a841012472c7303f8819423d8cc3b4530915de7265ebfa2/fonttools-4.60.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82cceceaf9c09a965a75b84a4b240dd3768e596ffb65ef53852681606fe7c9ba", size = 5002036, upload-time = "2025-12-09T13:37:42.639Z" }, + { url = "https://files.pythonhosted.org/packages/73/f6/ba6458f83ce1a9f8c3b17bd8f7b8a2205a126aac1055796b7e7cfebbd38f/fonttools-4.60.2-cp314-cp314t-win32.whl", hash = "sha256:bbfbc918a75437fe7e6d64d1b1e1f713237df1cf00f3a36dedae910b2ba01cee", size = 2330985, upload-time = "2025-12-09T13:37:45.157Z" }, + { url = "https://files.pythonhosted.org/packages/91/24/fea0ba4d3a32d4ed1103a1098bfd99dc78b5fe3bb97202920744a37b73dc/fonttools-4.60.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0e5cd9b0830f6550d58c84f3ab151a9892b50c4f9d538c5603c0ce6fff2eb3f1", size = 2396226, upload-time = "2025-12-09T13:37:47.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/a6d9446cb258d3fe87e311c2d7bacf8e8da3e5809fbdc3a8306db4f6b14e/fonttools-4.60.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a3c75b8b42f7f93906bdba9eb1197bb76aecbe9a0a7cf6feec75f7605b5e8008", size = 2857184, upload-time = "2025-12-09T13:37:49.96Z" }, + { url = "https://files.pythonhosted.org/packages/3a/f3/1b41d0b6a8b908aa07f652111155dd653ebbf0b3385e66562556c5206685/fonttools-4.60.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0f86c8c37bc0ec0b9c141d5e90c717ff614e93c187f06d80f18c7057097f71bc", size = 2401877, upload-time = "2025-12-09T13:37:52.307Z" }, + { url = "https://files.pythonhosted.org/packages/71/57/048fd781680c38b05c5463657d0d95d5f2391a51972176e175c01de29d42/fonttools-4.60.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe905403fe59683b0e9a45f234af2866834376b8821f34633b1c76fb731b6311", size = 4878073, upload-time = "2025-12-09T13:37:56.477Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/363364f052a893cebd3d449588b21244a9d873620fda03ad92702d2e1bc7/fonttools-4.60.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38ce703b60a906e421e12d9e3a7f064883f5e61bb23e8961f4be33cfe578500b", size = 4835385, upload-time = "2025-12-09T13:37:58.882Z" }, + { url = "https://files.pythonhosted.org/packages/1c/38/e392bb930b2436287e6021672345db26441bf1f85f1e98f8b9784334e41d/fonttools-4.60.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9e810c06f3e79185cecf120e58b343ea5a89b54dd695fd644446bcf8c026da5e", size = 4853084, upload-time = "2025-12-09T13:38:01.578Z" }, + { url = "https://files.pythonhosted.org/packages/65/60/0d77faeaecf7a3276a8a6dc49e2274357e6b3ed6a1774e2fdb2a7f142db0/fonttools-4.60.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:38faec8cc1d12122599814d15a402183f5123fb7608dac956121e7c6742aebc5", size = 4971144, upload-time = "2025-12-09T13:38:03.748Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c7/6d3ac3afbcd598631bce24c3ecb919e7d0644a82fea8ddc4454312fc0be6/fonttools-4.60.2-cp39-cp39-win32.whl", hash = "sha256:80a45cf7bf659acb7b36578f300231873daba67bd3ca8cce181c73f861f14a37", size = 1499411, upload-time = "2025-12-09T13:38:05.586Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/9dedf6420e23f9fa630bb97941839dddd2e1e57d1b2b85a902378dbe0bd2/fonttools-4.60.2-cp39-cp39-win_amd64.whl", hash = "sha256:c355d5972071938e1b1e0f5a1df001f68ecf1a62f34a3407dc8e0beccf052501", size = 1547943, upload-time = "2025-12-09T13:38:07.604Z" }, + { url = "https://files.pythonhosted.org/packages/79/6c/10280af05b44fafd1dff69422805061fa1af29270bc52dce031ac69540bf/fonttools-4.60.2-py3-none-any.whl", hash = "sha256:73cf92eeda67cf6ff10c8af56fc8f4f07c1647d989a979be9e388a49be26552a", size = 1144610, upload-time = "2025-12-09T13:38:09.5Z" }, +] + +[[package]] +name = "fonttools" +version = "4.61.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/94/8a28707adb00bed1bf22dac16ccafe60faf2ade353dcb32c3617ee917307/fonttools-4.61.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24", size = 2854799, upload-time = "2025-12-12T17:29:27.5Z" }, + { url = "https://files.pythonhosted.org/packages/94/93/c2e682faaa5ee92034818d8f8a8145ae73eb83619600495dcf8503fa7771/fonttools-4.61.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958", size = 2403032, upload-time = "2025-12-12T17:29:30.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/62/1748f7e7e1ee41aa52279fd2e3a6d0733dc42a673b16932bad8e5d0c8b28/fonttools-4.61.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8db08051fc9e7d8bc622f2112511b8107d8f27cd89e2f64ec45e9825e8288da", size = 4897863, upload-time = "2025-12-12T17:29:32.535Z" }, + { url = "https://files.pythonhosted.org/packages/69/69/4ca02ee367d2c98edcaeb83fc278d20972502ee071214ad9d8ca85e06080/fonttools-4.61.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a76d4cb80f41ba94a6691264be76435e5f72f2cb3cab0b092a6212855f71c2f6", size = 4859076, upload-time = "2025-12-12T17:29:34.907Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f5/660f9e3cefa078861a7f099107c6d203b568a6227eef163dd173bfc56bdc/fonttools-4.61.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a13fc8aeb24bad755eea8f7f9d409438eb94e82cf86b08fe77a03fbc8f6a96b1", size = 4875623, upload-time = "2025-12-12T17:29:37.33Z" }, + { url = "https://files.pythonhosted.org/packages/63/d1/9d7c5091d2276ed47795c131c1bf9316c3c1ab2789c22e2f59e0572ccd38/fonttools-4.61.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b846a1fcf8beadeb9ea4f44ec5bdde393e2f1569e17d700bfc49cd69bde75881", size = 4993327, upload-time = "2025-12-12T17:29:39.781Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2d/28def73837885ae32260d07660a052b99f0aa00454867d33745dfe49dbf0/fonttools-4.61.1-cp310-cp310-win32.whl", hash = "sha256:78a7d3ab09dc47ac1a363a493e6112d8cabed7ba7caad5f54dbe2f08676d1b47", size = 1502180, upload-time = "2025-12-12T17:29:42.217Z" }, + { url = "https://files.pythonhosted.org/packages/63/fa/bfdc98abb4dd2bd491033e85e3ba69a2313c850e759a6daa014bc9433b0f/fonttools-4.61.1-cp310-cp310-win_amd64.whl", hash = "sha256:eff1ac3cc66c2ac7cda1e64b4e2f3ffef474b7335f92fc3833fc632d595fcee6", size = 1550654, upload-time = "2025-12-12T17:29:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213, upload-time = "2025-12-12T17:29:46.675Z" }, + { url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689, upload-time = "2025-12-12T17:29:48.769Z" }, + { url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809, upload-time = "2025-12-12T17:29:51.701Z" }, + { url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039, upload-time = "2025-12-12T17:29:53.659Z" }, + { url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714, upload-time = "2025-12-12T17:29:55.592Z" }, + { url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648, upload-time = "2025-12-12T17:29:57.861Z" }, + { url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681, upload-time = "2025-12-12T17:29:59.943Z" }, + { url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951, upload-time = "2025-12-12T17:30:02.254Z" }, + { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" }, + { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" }, + { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" }, + { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454, upload-time = "2025-12-12T17:30:24.938Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191, upload-time = "2025-12-12T17:30:27.343Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410, upload-time = "2025-12-12T17:30:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/b0/8d/6fb3494dfe61a46258cd93d979cf4725ded4eb46c2a4ca35e4490d84daea/fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd", size = 4984460, upload-time = "2025-12-12T17:30:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/f7/f1/a47f1d30b3dc00d75e7af762652d4cbc3dff5c2697a0dbd5203c81afd9c3/fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3", size = 4925800, upload-time = "2025-12-12T17:30:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/a7/01/e6ae64a0981076e8a66906fab01539799546181e32a37a0257b77e4aa88b/fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d", size = 5067859, upload-time = "2025-12-12T17:30:36.593Z" }, + { url = "https://files.pythonhosted.org/packages/73/aa/28e40b8d6809a9b5075350a86779163f074d2b617c15d22343fce81918db/fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c", size = 2267821, upload-time = "2025-12-12T17:30:38.478Z" }, + { url = "https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b", size = 2318169, upload-time = "2025-12-12T17:30:40.951Z" }, + { url = "https://files.pythonhosted.org/packages/32/8f/4e7bf82c0cbb738d3c2206c920ca34ca74ef9dabde779030145d28665104/fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd", size = 2846094, upload-time = "2025-12-12T17:30:43.511Z" }, + { url = "https://files.pythonhosted.org/packages/71/09/d44e45d0a4f3a651f23a1e9d42de43bc643cce2971b19e784cc67d823676/fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e", size = 2396589, upload-time = "2025-12-12T17:30:45.681Z" }, + { url = "https://files.pythonhosted.org/packages/89/18/58c64cafcf8eb677a99ef593121f719e6dcbdb7d1c594ae5a10d4997ca8a/fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c", size = 4877892, upload-time = "2025-12-12T17:30:47.709Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ec/9e6b38c7ba1e09eb51db849d5450f4c05b7e78481f662c3b79dbde6f3d04/fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75", size = 4972884, upload-time = "2025-12-12T17:30:49.656Z" }, + { url = "https://files.pythonhosted.org/packages/5e/87/b5339da8e0256734ba0dbbf5b6cdebb1dd79b01dc8c270989b7bcd465541/fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063", size = 4924405, upload-time = "2025-12-12T17:30:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/0b/47/e3409f1e1e69c073a3a6fd8cb886eb18c0bae0ee13db2c8d5e7f8495e8b7/fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2", size = 5035553, upload-time = "2025-12-12T17:30:54.823Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b6/1f6600161b1073a984294c6c031e1a56ebf95b6164249eecf30012bb2e38/fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c", size = 2271915, upload-time = "2025-12-12T17:30:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/52/7b/91e7b01e37cc8eb0e1f770d08305b3655e4f002fc160fb82b3390eabacf5/fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c", size = 2323487, upload-time = "2025-12-12T17:30:59.804Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/908ad78e46c61c3e3ed70c3b58ff82ab48437faf84ec84f109592cabbd9f/fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa", size = 2929571, upload-time = "2025-12-12T17:31:02.574Z" }, + { url = "https://files.pythonhosted.org/packages/bd/41/975804132c6dea64cdbfbaa59f3518a21c137a10cccf962805b301ac6ab2/fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91", size = 2435317, upload-time = "2025-12-12T17:31:04.974Z" }, + { url = "https://files.pythonhosted.org/packages/b0/5a/aef2a0a8daf1ebaae4cfd83f84186d4a72ee08fd6a8451289fcd03ffa8a4/fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19", size = 4882124, upload-time = "2025-12-12T17:31:07.456Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/d6db3485b645b81cea538c9d1c9219d5805f0877fda18777add4671c5240/fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba", size = 5100391, upload-time = "2025-12-12T17:31:09.732Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d6/675ba631454043c75fcf76f0ca5463eac8eb0666ea1d7badae5fea001155/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7", size = 4978800, upload-time = "2025-12-12T17:31:11.681Z" }, + { url = "https://files.pythonhosted.org/packages/7f/33/d3ec753d547a8d2bdaedd390d4a814e8d5b45a093d558f025c6b990b554c/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118", size = 5006426, upload-time = "2025-12-12T17:31:13.764Z" }, + { url = "https://files.pythonhosted.org/packages/b4/40/cc11f378b561a67bea850ab50063366a0d1dd3f6d0a30ce0f874b0ad5664/fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5", size = 2335377, upload-time = "2025-12-12T17:31:16.49Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/c2/59/ae5cdac87a00962122ea37bb346d41b66aec05f9ce328fa2b9e216f8967b/frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47", size = 86967, upload-time = "2025-10-06T05:37:55.607Z" }, + { url = "https://files.pythonhosted.org/packages/8a/10/17059b2db5a032fd9323c41c39e9d1f5f9d0c8f04d1e4e3e788573086e61/frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca", size = 49984, upload-time = "2025-10-06T05:37:57.049Z" }, + { url = "https://files.pythonhosted.org/packages/4b/de/ad9d82ca8e5fa8f0c636e64606553c79e2b859ad253030b62a21fe9986f5/frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068", size = 50240, upload-time = "2025-10-06T05:37:58.145Z" }, + { url = "https://files.pythonhosted.org/packages/4e/45/3dfb7767c2a67d123650122b62ce13c731b6c745bc14424eea67678b508c/frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95", size = 219472, upload-time = "2025-10-06T05:37:59.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/bf/5bf23d913a741b960d5c1dac7c1985d8a2a1d015772b2d18ea168b08e7ff/frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459", size = 221531, upload-time = "2025-10-06T05:38:00.521Z" }, + { url = "https://files.pythonhosted.org/packages/d0/03/27ec393f3b55860859f4b74cdc8c2a4af3dbf3533305e8eacf48a4fd9a54/frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675", size = 219211, upload-time = "2025-10-06T05:38:01.842Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ad/0fd00c404fa73fe9b169429e9a972d5ed807973c40ab6b3cf9365a33d360/frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61", size = 231775, upload-time = "2025-10-06T05:38:03.384Z" }, + { url = "https://files.pythonhosted.org/packages/8a/c3/86962566154cb4d2995358bc8331bfc4ea19d07db1a96f64935a1607f2b6/frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6", size = 236631, upload-time = "2025-10-06T05:38:04.609Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/6ffad161dbd83782d2c66dc4d378a9103b31770cb1e67febf43aea42d202/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5", size = 218632, upload-time = "2025-10-06T05:38:05.917Z" }, + { url = "https://files.pythonhosted.org/packages/58/b2/4677eee46e0a97f9b30735e6ad0bf6aba3e497986066eb68807ac85cf60f/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3", size = 235967, upload-time = "2025-10-06T05:38:07.614Z" }, + { url = "https://files.pythonhosted.org/packages/05/f3/86e75f8639c5a93745ca7addbbc9de6af56aebb930d233512b17e46f6493/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1", size = 228799, upload-time = "2025-10-06T05:38:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/30/00/39aad3a7f0d98f5eb1d99a3c311215674ed87061aecee7851974b335c050/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178", size = 230566, upload-time = "2025-10-06T05:38:10.52Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4d/aa144cac44568d137846ddc4d5210fb5d9719eb1d7ec6fa2728a54b5b94a/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda", size = 217715, upload-time = "2025-10-06T05:38:11.832Z" }, + { url = "https://files.pythonhosted.org/packages/64/4c/8f665921667509d25a0dd72540513bc86b356c95541686f6442a3283019f/frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087", size = 39933, upload-time = "2025-10-06T05:38:13.061Z" }, + { url = "https://files.pythonhosted.org/packages/79/bd/bcc926f87027fad5e59926ff12d136e1082a115025d33c032d1cd69ab377/frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a", size = 44121, upload-time = "2025-10-06T05:38:14.572Z" }, + { url = "https://files.pythonhosted.org/packages/4c/07/9c2e4eb7584af4b705237b971b89a4155a8e57599c4483a131a39256a9a0/frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103", size = 40312, upload-time = "2025-10-06T05:38:15.699Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/24/7f/2747c0d332b9acfa75dc84447a066fdf812b5a6b8d30472b74d309bfe8cb/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59", size = 309285, upload-time = "2025-10-30T14:58:44.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966, upload-time = "2025-10-30T14:58:42.53Z" }, +] + +[package.optional-dependencies] +http = [ + { name = "aiohttp", marker = "python_full_version < '3.10'" }, +] + +[[package]] +name = "fsspec" +version = "2026.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, +] + +[package.optional-dependencies] +http = [ + { name = "aiohttp", marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/cb/9bb543bd987ffa1ee48202cc96a756951b734b79a542335c566148ade36c/hf_xet-1.3.2.tar.gz", hash = "sha256:e130ee08984783d12717444e538587fa2119385e5bd8fc2bb9f930419b73a7af", size = 643646, upload-time = "2026-02-27T17:26:08.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/75/462285971954269432aad2e7938c5c7ff9ec7d60129cec542ab37121e3d6/hf_xet-1.3.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:335a8f36c55fd35a92d0062f4e9201b4015057e62747b7e7001ffb203c0ee1d2", size = 3761019, upload-time = "2026-02-27T17:25:49.441Z" }, + { url = "https://files.pythonhosted.org/packages/35/56/987b0537ddaf88e17192ea09afa8eca853e55f39a4721578be436f8409df/hf_xet-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c1ae4d3a716afc774e66922f3cac8206bfa707db13f6a7e62dfff74bfc95c9a8", size = 3521565, upload-time = "2026-02-27T17:25:47.469Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5c/7e4a33a3d689f77761156cc34558047569e54af92e4d15a8f493229f6767/hf_xet-1.3.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6dbdf231efac0b9b39adcf12a07f0c030498f9212a18e8c50224d0e84ab803d", size = 4176494, upload-time = "2026-02-27T17:25:40.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b3/71e856bf9d9a69b3931837e8bf22e095775f268c8edcd4a9e8c355f92484/hf_xet-1.3.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c1980abfb68ecf6c1c7983379ed7b1e2b49a1aaf1a5aca9acc7d48e5e2e0a961", size = 3955601, upload-time = "2026-02-27T17:25:38.376Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/aecf97b3f0a981600a67ff4db15e2d433389d698a284bb0ea5d8fcdd6f7f/hf_xet-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1c88fbd90ad0d27c46b77a445f0a436ebaa94e14965c581123b68b1c52f5fd30", size = 4154770, upload-time = "2026-02-27T17:25:56.756Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e1/3af961f71a40e09bf5ee909842127b6b00f5ab4ee3817599dc0771b79893/hf_xet-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:35b855024ca37f2dd113ac1c08993e997fbe167b9d61f9ef66d3d4f84015e508", size = 4394161, upload-time = "2026-02-27T17:25:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c3/859509bade9178e21b8b1db867b8e10e9f817ab9ac1de77cb9f461ced765/hf_xet-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:31612ba0629046e425ba50375685a2586e11fb9144270ebabd75878c3eaf6378", size = 3637377, upload-time = "2026-02-27T17:26:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/05/7f/724cfbef4da92d577b71f68bf832961c8919f36c60d28d289a9fc9d024d4/hf_xet-1.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:433c77c9f4e132b562f37d66c9b22c05b5479f243a1f06a120c1c06ce8b1502a", size = 3497875, upload-time = "2026-02-27T17:26:09.034Z" }, + { url = "https://files.pythonhosted.org/packages/ba/75/9d54c1ae1d05fb704f977eca1671747babf1957f19f38ae75c5933bc2dc1/hf_xet-1.3.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:c34e2c7aefad15792d57067c1c89b2b02c1bbaeabd7f8456ae3d07b4bbaf4094", size = 3761076, upload-time = "2026-02-27T17:25:55.42Z" }, + { url = "https://files.pythonhosted.org/packages/f2/8a/08a24b6c6f52b5d26848c16e4b6d790bb810d1bf62c3505bed179f7032d3/hf_xet-1.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4bc995d6c41992831f762096020dc14a65fdf3963f86ffed580b596d04de32e3", size = 3521745, upload-time = "2026-02-27T17:25:54.217Z" }, + { url = "https://files.pythonhosted.org/packages/b5/db/a75cf400dd8a1a8acf226a12955ff6ee999f272dfc0505bafd8079a61267/hf_xet-1.3.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:959083c89dee30f7d6f890b36cdadda823386c4de63b1a30384a75bfd2ae995d", size = 4176301, upload-time = "2026-02-27T17:25:46.044Z" }, + { url = "https://files.pythonhosted.org/packages/01/40/6c4c798ffdd83e740dd3925c4e47793b07442a9efa3bc3866ba141a82365/hf_xet-1.3.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cfa760888633b08c01b398d212ce7e8c0d7adac6c86e4b20dfb2397d8acd78ee", size = 3955437, upload-time = "2026-02-27T17:25:44.703Z" }, + { url = "https://files.pythonhosted.org/packages/0c/09/9a3aa7c5f07d3e5cc57bb750d12a124ffa72c273a87164bd848f9ac5cc14/hf_xet-1.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3155a02e083aa21fd733a7485c7c36025e49d5975c8d6bda0453d224dd0b0ac4", size = 4154535, upload-time = "2026-02-27T17:26:05.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e0/831f7fa6d90cb47a230bc23284b502c700e1483bbe459437b3844cdc0776/hf_xet-1.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:91b1dc03c31cbf733d35dc03df7c5353686233d86af045e716f1e0ea4a2673cf", size = 4393891, upload-time = "2026-02-27T17:26:06.607Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/6ed472fdce7f8b70f5da6e3f05be76816a610063003bfd6d9cea0bbb58a3/hf_xet-1.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:211f30098512d95e85ad03ae63bd7dd2c4df476558a5095d09f9e38e78cbf674", size = 3637583, upload-time = "2026-02-27T17:26:17.349Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/a069edc4570b3f8e123c0b80fadc94530f3d7b01394e1fc1bb223339366c/hf_xet-1.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:4a6817c41de7c48ed9270da0b02849347e089c5ece9a0e72ae4f4b3a57617f82", size = 3497977, upload-time = "2026-02-27T17:26:14.966Z" }, + { url = "https://files.pythonhosted.org/packages/d8/28/dbb024e2e3907f6f3052847ca7d1a2f7a3972fafcd53ff79018977fcb3e4/hf_xet-1.3.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f93b7595f1d8fefddfede775c18b5c9256757824f7f6832930b49858483cd56f", size = 3763961, upload-time = "2026-02-27T17:25:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/e4/71/b99aed3823c9d1795e4865cf437d651097356a3f38c7d5877e4ac544b8e4/hf_xet-1.3.2-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:a85d3d43743174393afe27835bde0cd146e652b5fcfdbcd624602daef2ef3259", size = 3526171, upload-time = "2026-02-27T17:25:50.968Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ca/907890ce6ef5598b5920514f255ed0a65f558f820515b18db75a51b2f878/hf_xet-1.3.2-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7c2a054a97c44e136b1f7f5a78f12b3efffdf2eed3abc6746fc5ea4b39511633", size = 4180750, upload-time = "2026-02-27T17:25:43.125Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ad/bc7f41f87173d51d0bce497b171c4ee0cbde1eed2d7b4216db5d0ada9f50/hf_xet-1.3.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:06b724a361f670ae557836e57801b82c75b534812e351a87a2c739f77d1e0635", size = 3961035, upload-time = "2026-02-27T17:25:41.837Z" }, + { url = "https://files.pythonhosted.org/packages/73/38/600f4dda40c4a33133404d9fe644f1d35ff2d9babb4d0435c646c63dd107/hf_xet-1.3.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:305f5489d7241a47e0458ef49334be02411d1d0f480846363c1c8084ed9916f7", size = 4161378, upload-time = "2026-02-27T17:26:00.365Z" }, + { url = "https://files.pythonhosted.org/packages/00/b3/7bc1ff91d1ac18420b7ad1e169b618b27c00001b96310a89f8a9294fe509/hf_xet-1.3.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:06cdbde243c85f39a63b28e9034321399c507bcd5e7befdd17ed2ccc06dfe14e", size = 4398020, upload-time = "2026-02-27T17:26:03.977Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/99bfd948a3ed3620ab709276df3ad3710dcea61976918cce8706502927af/hf_xet-1.3.2-cp37-abi3-win_amd64.whl", hash = "sha256:9298b47cce6037b7045ae41482e703c471ce36b52e73e49f71226d2e8e5685a1", size = 3641624, upload-time = "2026-02-27T17:26:13.542Z" }, + { url = "https://files.pythonhosted.org/packages/cc/02/9a6e4ca1f3f73a164c0cd48e41b3cc56585dcc37e809250de443d673266f/hf_xet-1.3.2-cp37-abi3-win_arm64.whl", hash = "sha256:83d8ec273136171431833a6957e8f3af496bee227a0fe47c7b8b39c106d1749a", size = 3503976, upload-time = "2026-02-27T17:26:12.123Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "filelock", version = "3.25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "fsspec", version = "2025.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "fsspec", version = "2026.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typer", version = "0.23.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "typer", version = "0.24.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/7a/304cec37112382c4fe29a43bcb0d5891f922785d18745883d2aa4eb74e4b/huggingface_hub-1.6.0.tar.gz", hash = "sha256:d931ddad8ba8dfc1e816bf254810eb6f38e5c32f60d4184b5885662a3b167325", size = 717071, upload-time = "2026-03-06T14:19:18.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/e3/e3a44f54c8e2f28983fcf07f13d4260b37bd6a0d3a081041bc60b91d230e/huggingface_hub-1.6.0-py3-none-any.whl", hash = "sha256:ef40e2d5cb85e48b2c067020fa5142168342d5108a1b267478ed384ecbf18961", size = 612874, upload-time = "2026-03-06T14:19:16.844Z" }, +] + +[[package]] +name = "identify" +version = "2.6.15" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, +] + +[[package]] +name = "identify" +version = "2.6.17" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/57/84/376a3b96e5a8d33a7aa2c5b3b31a4b3c364117184bf0b17418055f6ace66/identify-2.6.17.tar.gz", hash = "sha256:f816b0b596b204c9fdf076ded172322f2723cf958d02f9c3587504834c8ff04d", size = 99579, upload-time = "2026-03-01T20:04:12.702Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/66/71c1227dff78aaeb942fed29dd5651f2aec166cc7c9aeea3e8b26a539b7d/identify-2.6.17-py2.py3-none-any.whl", hash = "sha256:be5f8412d5ed4b20f2bd41a65f920990bdccaa6a4a18a08f1eefdcd0bdd885f0", size = 99382, upload-time = "2026-03-01T20:04:11.439Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "importlib-resources" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "ipykernel" +version = "6.31.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "appnope", marker = "python_full_version < '3.10' and sys_platform == 'darwin'" }, + { name = "comm", marker = "python_full_version < '3.10'" }, + { name = "debugpy", marker = "python_full_version < '3.10'" }, + { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jupyter-client", version = "8.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.10'" }, + { name = "nest-asyncio", marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "psutil", marker = "python_full_version < '3.10'" }, + { name = "pyzmq", marker = "python_full_version < '3.10'" }, + { name = "tornado", marker = "python_full_version < '3.10'" }, + { name = "traitlets", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/1d/d5ba6edbfe6fae4c3105bca3a9c889563cc752c7f2de45e333164c7f4846/ipykernel-6.31.0.tar.gz", hash = "sha256:2372ce8bc1ff4f34e58cafed3a0feb2194b91fc7cad0fc72e79e47b45ee9e8f6", size = 167493, upload-time = "2025-10-20T11:42:39.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl", hash = "sha256:abe5386f6ced727a70e0eb0cf1da801fa7c5fa6ff82147747d5a0406cd8c94af", size = 117003, upload-time = "2025-10-20T11:42:37.502Z" }, +] + +[[package]] +name = "ipykernel" +version = "7.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "appnope", marker = "python_full_version >= '3.10' and sys_platform == 'darwin'" }, + { name = "comm", marker = "python_full_version >= '3.10'" }, + { name = "debugpy", marker = "python_full_version >= '3.10'" }, + { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "ipython", version = "9.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "jupyter-client", version = "8.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.10'" }, + { name = "nest-asyncio", marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "psutil", marker = "python_full_version >= '3.10'" }, + { name = "pyzmq", marker = "python_full_version >= '3.10'" }, + { name = "tornado", marker = "python_full_version >= '3.10'" }, + { name = "traitlets", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/8d/b68b728e2d06b9e0051019640a40a9eb7a88fcd82c2e1b5ce70bef5ff044/ipykernel-7.2.0.tar.gz", hash = "sha256:18ed160b6dee2cbb16e5f3575858bc19d8f1fe6046a9a680c708494ce31d909e", size = 176046, upload-time = "2026-02-06T16:43:27.403Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl", hash = "sha256:3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661", size = 118788, upload-time = "2026-02-06T16:43:25.149Z" }, +] + +[[package]] +name = "ipython" +version = "8.18.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.10'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "jedi", marker = "python_full_version < '3.10'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.10'" }, + { name = "pexpect", marker = "python_full_version < '3.10' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "stack-data", marker = "python_full_version < '3.10'" }, + { name = "traitlets", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/b9/3ba6c45a6df813c09a48bac313c22ff83efa26cbb55011218d925a46e2ad/ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27", size = 5486330, upload-time = "2023-11-27T09:58:34.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/6b/d9fdcdef2eb6a23f391251fde8781c38d42acd82abe84d054cb74f7863b0/ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397", size = 808161, upload-time = "2023-11-27T09:58:30.538Z" }, +] + +[[package]] +name = "ipython" +version = "8.38.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version == '3.10.*'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "jedi", marker = "python_full_version == '3.10.*'" }, + { name = "matplotlib-inline", marker = "python_full_version == '3.10.*'" }, + { name = "pexpect", marker = "python_full_version == '3.10.*' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version == '3.10.*'" }, + { name = "pygments", marker = "python_full_version == '3.10.*'" }, + { name = "stack-data", marker = "python_full_version == '3.10.*'" }, + { name = "traitlets", marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/61/1810830e8b93c72dcd3c0f150c80a00c3deb229562d9423807ec92c3a539/ipython-8.38.0.tar.gz", hash = "sha256:9cfea8c903ce0867cc2f23199ed8545eb741f3a69420bfcf3743ad1cec856d39", size = 5513996, upload-time = "2026-01-05T10:59:06.901Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/df/db59624f4c71b39717c423409950ac3f2c8b2ce4b0aac843112c7fb3f721/ipython-8.38.0-py3-none-any.whl", hash = "sha256:750162629d800ac65bb3b543a14e7a74b0e88063eac9b92124d4b2aa3f6d8e86", size = 831813, upload-time = "2026-01-05T10:59:04.239Z" }, +] + +[[package]] +name = "ipython" +version = "9.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version == '3.11.*'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version == '3.11.*'" }, + { name = "jedi", marker = "python_full_version == '3.11.*'" }, + { name = "matplotlib-inline", marker = "python_full_version == '3.11.*'" }, + { name = "pexpect", marker = "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version == '3.11.*'" }, + { name = "pygments", marker = "python_full_version == '3.11.*'" }, + { name = "stack-data", marker = "python_full_version == '3.11.*'" }, + { name = "traitlets", marker = "python_full_version == '3.11.*'" }, + { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/60/2111715ea11f39b1535bed6024b7dec7918b71e5e5d30855a5b503056b50/ipython-9.10.0.tar.gz", hash = "sha256:cd9e656be97618a0676d058134cd44e6dc7012c0e5cb36a9ce96a8c904adaf77", size = 4426526, upload-time = "2026-02-02T10:00:33.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl", hash = "sha256:c6ab68cc23bba8c7e18e9b932797014cc61ea7fd6f19de180ab9ba73e65ee58d", size = 622774, upload-time = "2026-02-02T10:00:31.503Z" }, +] + +[[package]] +name = "ipython" +version = "9.11.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version >= '3.12'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.12'" }, + { name = "jedi", marker = "python_full_version >= '3.12'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.12'" }, + { name = "pexpect", marker = "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "stack-data", marker = "python_full_version >= '3.12'" }, + { name = "traitlets", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/28/a4698eda5a8928a45d6b693578b135b753e14fa1c2b36ee9441e69a45576/ipython-9.11.0.tar.gz", hash = "sha256:2a94bc4406b22ecc7e4cb95b98450f3ea493a76bec8896cda11b78d7752a6667", size = 4427354, upload-time = "2026-03-05T08:57:30.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/90/45c72becc57158facc6a6404f663b77bbcea2519ca57f760e2879ae1315d/ipython-9.11.0-py3-none-any.whl", hash = "sha256:6922d5bcf944c6e525a76a0a304451b60a2b6f875e86656d8bc2dfda5d710e19", size = 624222, upload-time = "2026-03-05T08:57:28.94Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + +[[package]] +name = "jinja2" +version = "2.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/2f/0b98b06a345a761bec91a079ccae392d282690c2d8272e708f4d10829e22/Jinja2-2.8.tar.gz", hash = "sha256:bc1ff2ff88dbfacefde4ddde471d1417d3b304e8df103a7a9437d47269201bf4", size = 357055, upload-time = "2015-07-26T17:49:47.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/a1/c56bc4d99dc2663514a8481511e80eba8994133ae75eebdadfc91a5597d9/Jinja2-2.8-py2.py3-none-any.whl", hash = "sha256:1cc03ef32b64be19e0a5b54578dd790906a34943fe9102cfdae0d4495bd536b4", size = 263888, upload-time = "2015-07-26T17:52:06.373Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "python-dateutil", marker = "python_full_version < '3.10'" }, + { name = "pyzmq", marker = "python_full_version < '3.10'" }, + { name = "tornado", marker = "python_full_version < '3.10'" }, + { name = "traitlets", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload-time = "2024-09-17T10:44:17.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105, upload-time = "2024-09-17T10:44:15.218Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, + { name = "pyzmq", marker = "python_full_version >= '3.10'" }, + { name = "tornado", marker = "python_full_version >= '3.10'" }, + { name = "traitlets", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.8.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pywin32", marker = "python_full_version < '3.10' and platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, + { name = "traitlets", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/1b/72906d554acfeb588332eaaa6f61577705e9ec752ddb486f302dafa292d9/jupyter_core-5.8.1.tar.gz", hash = "sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941", size = 88923, upload-time = "2025-05-27T07:38:16.655Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/57/6bffd4b20b88da3800c5d691e0337761576ee688eb01299eae865689d2df/jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0", size = 28880, upload-time = "2025-05-27T07:38:15.137Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "platformdirs", version = "4.9.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "traitlets", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286, upload-time = "2024-09-04T09:39:44.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/14/fc943dd65268a96347472b4fbe5dcc2f6f55034516f80576cd0dd3a8930f/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6", size = 122440, upload-time = "2024-09-04T09:03:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/1e/46/e68fed66236b69dd02fcdb506218c05ac0e39745d696d22709498896875d/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17", size = 65758, upload-time = "2024-09-04T09:03:46.582Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fa/65de49c85838681fc9cb05de2a68067a683717321e01ddafb5b8024286f0/kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9", size = 64311, upload-time = "2024-09-04T09:03:47.973Z" }, + { url = "https://files.pythonhosted.org/packages/42/9c/cc8d90f6ef550f65443bad5872ffa68f3dee36de4974768628bea7c14979/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9", size = 1637109, upload-time = "2024-09-04T09:03:49.281Z" }, + { url = "https://files.pythonhosted.org/packages/55/91/0a57ce324caf2ff5403edab71c508dd8f648094b18cfbb4c8cc0fde4a6ac/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c", size = 1617814, upload-time = "2024-09-04T09:03:51.444Z" }, + { url = "https://files.pythonhosted.org/packages/12/5d/c36140313f2510e20207708adf36ae4919416d697ee0236b0ddfb6fd1050/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599", size = 1400881, upload-time = "2024-09-04T09:03:53.357Z" }, + { url = "https://files.pythonhosted.org/packages/56/d0/786e524f9ed648324a466ca8df86298780ef2b29c25313d9a4f16992d3cf/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05", size = 1512972, upload-time = "2024-09-04T09:03:55.082Z" }, + { url = "https://files.pythonhosted.org/packages/67/5a/77851f2f201e6141d63c10a0708e996a1363efaf9e1609ad0441b343763b/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407", size = 1444787, upload-time = "2024-09-04T09:03:56.588Z" }, + { url = "https://files.pythonhosted.org/packages/06/5f/1f5eaab84355885e224a6fc8d73089e8713dc7e91c121f00b9a1c58a2195/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278", size = 2199212, upload-time = "2024-09-04T09:03:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/b5/28/9152a3bfe976a0ae21d445415defc9d1cd8614b2910b7614b30b27a47270/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5", size = 2346399, upload-time = "2024-09-04T09:04:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/f6/453d1904c52ac3b400f4d5e240ac5fec25263716723e44be65f4d7149d13/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad", size = 2308688, upload-time = "2024-09-04T09:04:02.216Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/d4968499441b9ae187e81745e3277a8b4d7c60840a52dc9d535a7909fac3/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895", size = 2445493, upload-time = "2024-09-04T09:04:04.571Z" }, + { url = "https://files.pythonhosted.org/packages/07/c9/032267192e7828520dacb64dfdb1d74f292765f179e467c1cba97687f17d/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3", size = 2262191, upload-time = "2024-09-04T09:04:05.969Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ad/db0aedb638a58b2951da46ddaeecf204be8b4f5454df020d850c7fa8dca8/kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc", size = 46644, upload-time = "2024-09-04T09:04:07.408Z" }, + { url = "https://files.pythonhosted.org/packages/12/ca/d0f7b7ffbb0be1e7c2258b53554efec1fd652921f10d7d85045aff93ab61/kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c", size = 55877, upload-time = "2024-09-04T09:04:08.869Z" }, + { url = "https://files.pythonhosted.org/packages/97/6c/cfcc128672f47a3e3c0d918ecb67830600078b025bfc32d858f2e2d5c6a4/kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a", size = 48347, upload-time = "2024-09-04T09:04:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77429fa0a58f941d6e1c58da9efe08597d2e86bf2b2cce6626834f49d07b/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", size = 122442, upload-time = "2024-09-04T09:04:11.432Z" }, + { url = "https://files.pythonhosted.org/packages/e5/20/8c75caed8f2462d63c7fd65e16c832b8f76cda331ac9e615e914ee80bac9/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", size = 65762, upload-time = "2024-09-04T09:04:12.468Z" }, + { url = "https://files.pythonhosted.org/packages/f4/98/fe010f15dc7230f45bc4cf367b012d651367fd203caaa992fd1f5963560e/kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", size = 64319, upload-time = "2024-09-04T09:04:13.635Z" }, + { url = "https://files.pythonhosted.org/packages/8b/1b/b5d618f4e58c0675654c1e5051bcf42c776703edb21c02b8c74135541f60/kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", size = 1334260, upload-time = "2024-09-04T09:04:14.878Z" }, + { url = "https://files.pythonhosted.org/packages/b8/01/946852b13057a162a8c32c4c8d2e9ed79f0bb5d86569a40c0b5fb103e373/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", size = 1426589, upload-time = "2024-09-04T09:04:16.514Z" }, + { url = "https://files.pythonhosted.org/packages/70/d1/c9f96df26b459e15cf8a965304e6e6f4eb291e0f7a9460b4ad97b047561e/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", size = 1541080, upload-time = "2024-09-04T09:04:18.322Z" }, + { url = "https://files.pythonhosted.org/packages/d3/73/2686990eb8b02d05f3de759d6a23a4ee7d491e659007dd4c075fede4b5d0/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052", size = 1470049, upload-time = "2024-09-04T09:04:20.266Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4b/2db7af3ed3af7c35f388d5f53c28e155cd402a55432d800c543dc6deb731/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", size = 1426376, upload-time = "2024-09-04T09:04:22.419Z" }, + { url = "https://files.pythonhosted.org/packages/05/83/2857317d04ea46dc5d115f0df7e676997bbd968ced8e2bd6f7f19cfc8d7f/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", size = 2222231, upload-time = "2024-09-04T09:04:24.526Z" }, + { url = "https://files.pythonhosted.org/packages/0d/b5/866f86f5897cd4ab6d25d22e403404766a123f138bd6a02ecb2cdde52c18/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", size = 2368634, upload-time = "2024-09-04T09:04:25.899Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ee/73de8385403faba55f782a41260210528fe3273d0cddcf6d51648202d6d0/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", size = 2329024, upload-time = "2024-09-04T09:04:28.523Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/cd101d8cd2cdfaa42dc06c433df17c8303d31129c9fdd16c0ea37672af91/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", size = 2468484, upload-time = "2024-09-04T09:04:30.547Z" }, + { url = "https://files.pythonhosted.org/packages/e1/72/84f09d45a10bc57a40bb58b81b99d8f22b58b2040c912b7eb97ebf625bf2/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", size = 2284078, upload-time = "2024-09-04T09:04:33.218Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d4/71828f32b956612dc36efd7be1788980cb1e66bfb3706e6dec9acad9b4f9/kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", size = 46645, upload-time = "2024-09-04T09:04:34.371Z" }, + { url = "https://files.pythonhosted.org/packages/a1/65/d43e9a20aabcf2e798ad1aff6c143ae3a42cf506754bcb6a7ed8259c8425/kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", size = 56022, upload-time = "2024-09-04T09:04:35.786Z" }, + { url = "https://files.pythonhosted.org/packages/35/b3/9f75a2e06f1b4ca00b2b192bc2b739334127d27f1d0625627ff8479302ba/kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", size = 48536, upload-time = "2024-09-04T09:04:37.525Z" }, + { url = "https://files.pythonhosted.org/packages/97/9c/0a11c714cf8b6ef91001c8212c4ef207f772dd84540104952c45c1f0a249/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", size = 121808, upload-time = "2024-09-04T09:04:38.637Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d8/0fe8c5f5d35878ddd135f44f2af0e4e1d379e1c7b0716f97cdcb88d4fd27/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", size = 65531, upload-time = "2024-09-04T09:04:39.694Z" }, + { url = "https://files.pythonhosted.org/packages/80/c5/57fa58276dfdfa612241d640a64ca2f76adc6ffcebdbd135b4ef60095098/kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", size = 63894, upload-time = "2024-09-04T09:04:41.6Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e9/26d3edd4c4ad1c5b891d8747a4f81b1b0aba9fb9721de6600a4adc09773b/kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", size = 1369296, upload-time = "2024-09-04T09:04:42.886Z" }, + { url = "https://files.pythonhosted.org/packages/b6/67/3f4850b5e6cffb75ec40577ddf54f7b82b15269cc5097ff2e968ee32ea7d/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", size = 1461450, upload-time = "2024-09-04T09:04:46.284Z" }, + { url = "https://files.pythonhosted.org/packages/52/be/86cbb9c9a315e98a8dc6b1d23c43cffd91d97d49318854f9c37b0e41cd68/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", size = 1579168, upload-time = "2024-09-04T09:04:47.91Z" }, + { url = "https://files.pythonhosted.org/packages/0f/00/65061acf64bd5fd34c1f4ae53f20b43b0a017a541f242a60b135b9d1e301/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", size = 1507308, upload-time = "2024-09-04T09:04:49.465Z" }, + { url = "https://files.pythonhosted.org/packages/21/e4/c0b6746fd2eb62fe702118b3ca0cb384ce95e1261cfada58ff693aeec08a/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", size = 1464186, upload-time = "2024-09-04T09:04:50.949Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0f/529d0a9fffb4d514f2782c829b0b4b371f7f441d61aa55f1de1c614c4ef3/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", size = 2247877, upload-time = "2024-09-04T09:04:52.388Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e1/66603ad779258843036d45adcbe1af0d1a889a07af4635f8b4ec7dccda35/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", size = 2404204, upload-time = "2024-09-04T09:04:54.385Z" }, + { url = "https://files.pythonhosted.org/packages/8d/61/de5fb1ca7ad1f9ab7970e340a5b833d735df24689047de6ae71ab9d8d0e7/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", size = 2352461, upload-time = "2024-09-04T09:04:56.307Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d2/0edc00a852e369827f7e05fd008275f550353f1f9bcd55db9363d779fc63/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", size = 2501358, upload-time = "2024-09-04T09:04:57.922Z" }, + { url = "https://files.pythonhosted.org/packages/84/15/adc15a483506aec6986c01fb7f237c3aec4d9ed4ac10b756e98a76835933/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", size = 2314119, upload-time = "2024-09-04T09:04:59.332Z" }, + { url = "https://files.pythonhosted.org/packages/36/08/3a5bb2c53c89660863a5aa1ee236912269f2af8762af04a2e11df851d7b2/kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", size = 46367, upload-time = "2024-09-04T09:05:00.804Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/c05f0a6d825c643779fc3c70876bff1ac221f0e31e6f701f0e9578690d70/kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", size = 55884, upload-time = "2024-09-04T09:05:01.924Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f9/3828d8f21b6de4279f0667fb50a9f5215e6fe57d5ec0d61905914f5b6099/kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", size = 48528, upload-time = "2024-09-04T09:05:02.983Z" }, + { url = "https://files.pythonhosted.org/packages/c4/06/7da99b04259b0f18b557a4effd1b9c901a747f7fdd84cf834ccf520cb0b2/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e", size = 121913, upload-time = "2024-09-04T09:05:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/97/f5/b8a370d1aa593c17882af0a6f6755aaecd643640c0ed72dcfd2eafc388b9/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6", size = 65627, upload-time = "2024-09-04T09:05:05.119Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fc/6c0374f7503522539e2d4d1b497f5ebad3f8ed07ab51aed2af988dd0fb65/kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750", size = 63888, upload-time = "2024-09-04T09:05:06.191Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3e/0b7172793d0f41cae5c923492da89a2ffcd1adf764c16159ca047463ebd3/kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d", size = 1369145, upload-time = "2024-09-04T09:05:07.919Z" }, + { url = "https://files.pythonhosted.org/packages/77/92/47d050d6f6aced2d634258123f2688fbfef8ded3c5baf2c79d94d91f1f58/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379", size = 1461448, upload-time = "2024-09-04T09:05:10.01Z" }, + { url = "https://files.pythonhosted.org/packages/9c/1b/8f80b18e20b3b294546a1adb41701e79ae21915f4175f311a90d042301cf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c", size = 1578750, upload-time = "2024-09-04T09:05:11.598Z" }, + { url = "https://files.pythonhosted.org/packages/a4/fe/fe8e72f3be0a844f257cadd72689c0848c6d5c51bc1d60429e2d14ad776e/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34", size = 1507175, upload-time = "2024-09-04T09:05:13.22Z" }, + { url = "https://files.pythonhosted.org/packages/39/fa/cdc0b6105d90eadc3bee525fecc9179e2b41e1ce0293caaf49cb631a6aaf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1", size = 1463963, upload-time = "2024-09-04T09:05:15.925Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5c/0c03c4e542720c6177d4f408e56d1c8315899db72d46261a4e15b8b33a41/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f", size = 2248220, upload-time = "2024-09-04T09:05:17.434Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ee/55ef86d5a574f4e767df7da3a3a7ff4954c996e12d4fbe9c408170cd7dcc/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b", size = 2404463, upload-time = "2024-09-04T09:05:18.997Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6d/73ad36170b4bff4825dc588acf4f3e6319cb97cd1fb3eb04d9faa6b6f212/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27", size = 2352842, upload-time = "2024-09-04T09:05:21.299Z" }, + { url = "https://files.pythonhosted.org/packages/0b/16/fa531ff9199d3b6473bb4d0f47416cdb08d556c03b8bc1cccf04e756b56d/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a", size = 2501635, upload-time = "2024-09-04T09:05:23.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/7e/aa9422e78419db0cbe75fb86d8e72b433818f2e62e2e394992d23d23a583/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee", size = 2314556, upload-time = "2024-09-04T09:05:25.907Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/15f7f556df0a6e5b3772a1e076a9d9f6c538ce5f05bd590eca8106508e06/kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07", size = 46364, upload-time = "2024-09-04T09:05:27.184Z" }, + { url = "https://files.pythonhosted.org/packages/0b/db/32e897e43a330eee8e4770bfd2737a9584b23e33587a0812b8e20aac38f7/kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76", size = 55887, upload-time = "2024-09-04T09:05:28.372Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a4/df2bdca5270ca85fd25253049eb6708d4127be2ed0e5c2650217450b59e9/kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650", size = 48530, upload-time = "2024-09-04T09:05:30.225Z" }, + { url = "https://files.pythonhosted.org/packages/11/88/37ea0ea64512997b13d69772db8dcdc3bfca5442cda3a5e4bb943652ee3e/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd", size = 122449, upload-time = "2024-09-04T09:05:55.311Z" }, + { url = "https://files.pythonhosted.org/packages/4e/45/5a5c46078362cb3882dcacad687c503089263c017ca1241e0483857791eb/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583", size = 65757, upload-time = "2024-09-04T09:05:56.906Z" }, + { url = "https://files.pythonhosted.org/packages/8a/be/a6ae58978772f685d48dd2e84460937761c53c4bbd84e42b0336473d9775/kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417", size = 64312, upload-time = "2024-09-04T09:05:58.384Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/18ef6f452d311e1e1eb180c9bf5589187fa1f042db877e6fe443ef10099c/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904", size = 1626966, upload-time = "2024-09-04T09:05:59.855Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/40655f6c3fa11ce740e8a964fa8e4c0479c87d6a7944b95af799c7a55dfe/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a", size = 1607044, upload-time = "2024-09-04T09:06:02.16Z" }, + { url = "https://files.pythonhosted.org/packages/fd/93/af67dbcfb9b3323bbd2c2db1385a7139d8f77630e4a37bb945b57188eb2d/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8", size = 1391879, upload-time = "2024-09-04T09:06:03.908Z" }, + { url = "https://files.pythonhosted.org/packages/40/6f/d60770ef98e77b365d96061d090c0cd9e23418121c55fff188fa4bdf0b54/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2", size = 1504751, upload-time = "2024-09-04T09:06:05.58Z" }, + { url = "https://files.pythonhosted.org/packages/fa/3a/5f38667d313e983c432f3fcd86932177519ed8790c724e07d77d1de0188a/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88", size = 1436990, upload-time = "2024-09-04T09:06:08.126Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/1520301a47326e6a6043b502647e42892be33b3f051e9791cc8bb43f1a32/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde", size = 2191122, upload-time = "2024-09-04T09:06:10.345Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c4/eb52da300c166239a2233f1f9c4a1b767dfab98fae27681bfb7ea4873cb6/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c", size = 2338126, upload-time = "2024-09-04T09:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/1a/cb/42b92fd5eadd708dd9107c089e817945500685f3437ce1fd387efebc6d6e/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2", size = 2298313, upload-time = "2024-09-04T09:06:14.562Z" }, + { url = "https://files.pythonhosted.org/packages/4f/eb/be25aa791fe5fc75a8b1e0c965e00f942496bc04635c9aae8035f6b76dcd/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb", size = 2437784, upload-time = "2024-09-04T09:06:16.767Z" }, + { url = "https://files.pythonhosted.org/packages/c5/22/30a66be7f3368d76ff95689e1c2e28d382383952964ab15330a15d8bfd03/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327", size = 2253988, upload-time = "2024-09-04T09:06:18.705Z" }, + { url = "https://files.pythonhosted.org/packages/35/d3/5f2ecb94b5211c8a04f218a76133cc8d6d153b0f9cd0b45fad79907f0689/kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644", size = 46980, upload-time = "2024-09-04T09:06:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/ef/17/cd10d020578764ea91740204edc6b3236ed8106228a46f568d716b11feb2/kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4", size = 55847, upload-time = "2024-09-04T09:06:21.407Z" }, + { url = "https://files.pythonhosted.org/packages/91/84/32232502020bd78d1d12be7afde15811c64a95ed1f606c10456db4e4c3ac/kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f", size = 48494, upload-time = "2024-09-04T09:06:22.648Z" }, + { url = "https://files.pythonhosted.org/packages/ac/59/741b79775d67ab67ced9bb38552da688c0305c16e7ee24bba7a2be253fb7/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643", size = 59491, upload-time = "2024-09-04T09:06:24.188Z" }, + { url = "https://files.pythonhosted.org/packages/58/cc/fb239294c29a5656e99e3527f7369b174dd9cc7c3ef2dea7cb3c54a8737b/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706", size = 57648, upload-time = "2024-09-04T09:06:25.559Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ef/2f009ac1f7aab9f81efb2d837301d255279d618d27b6015780115ac64bdd/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6", size = 84257, upload-time = "2024-09-04T09:06:27.038Z" }, + { url = "https://files.pythonhosted.org/packages/81/e1/c64f50987f85b68b1c52b464bb5bf73e71570c0f7782d626d1eb283ad620/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2", size = 80906, upload-time = "2024-09-04T09:06:28.48Z" }, + { url = "https://files.pythonhosted.org/packages/fd/71/1687c5c0a0be2cee39a5c9c389e546f9c6e215e46b691d00d9f646892083/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4", size = 79951, upload-time = "2024-09-04T09:06:29.966Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8b/d7497df4a1cae9367adf21665dd1f896c2a7aeb8769ad77b662c5e2bcce7/kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a", size = 55715, upload-time = "2024-09-04T09:06:31.489Z" }, + { url = "https://files.pythonhosted.org/packages/d5/df/ce37d9b26f07ab90880923c94d12a6ff4d27447096b4c849bfc4339ccfdf/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39", size = 58666, upload-time = "2024-09-04T09:06:43.756Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d3/e4b04f43bc629ac8e186b77b2b1a251cdfa5b7610fa189dc0db622672ce6/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e", size = 57088, upload-time = "2024-09-04T09:06:45.406Z" }, + { url = "https://files.pythonhosted.org/packages/30/1c/752df58e2d339e670a535514d2db4fe8c842ce459776b8080fbe08ebb98e/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608", size = 84321, upload-time = "2024-09-04T09:06:47.557Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f8/fe6484e847bc6e238ec9f9828089fb2c0bb53f2f5f3a79351fde5b565e4f/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674", size = 80776, upload-time = "2024-09-04T09:06:49.235Z" }, + { url = "https://files.pythonhosted.org/packages/9b/57/d7163c0379f250ef763aba85330a19feefb5ce6cb541ade853aaba881524/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225", size = 79984, upload-time = "2024-09-04T09:06:51.336Z" }, + { url = "https://files.pythonhosted.org/packages/8c/95/4a103776c265d13b3d2cd24fb0494d4e04ea435a8ef97e1b2c026d43250b/kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0", size = 55811, upload-time = "2024-09-04T09:06:53.078Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/5d/8ce64e36d4e3aac5ca96996457dcf33e34e6051492399a3f1fec5657f30b/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b", size = 124159, upload-time = "2025-08-10T21:25:35.472Z" }, + { url = "https://files.pythonhosted.org/packages/96/1e/22f63ec454874378175a5f435d6ea1363dd33fb2af832c6643e4ccea0dc8/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f", size = 66578, upload-time = "2025-08-10T21:25:36.73Z" }, + { url = "https://files.pythonhosted.org/packages/41/4c/1925dcfff47a02d465121967b95151c82d11027d5ec5242771e580e731bd/kiwisolver-1.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf", size = 65312, upload-time = "2025-08-10T21:25:37.658Z" }, + { url = "https://files.pythonhosted.org/packages/d4/42/0f333164e6307a0687d1eb9ad256215aae2f4bd5d28f4653d6cd319a3ba3/kiwisolver-1.4.9-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9", size = 1628458, upload-time = "2025-08-10T21:25:39.067Z" }, + { url = "https://files.pythonhosted.org/packages/86/b6/2dccb977d651943995a90bfe3495c2ab2ba5cd77093d9f2318a20c9a6f59/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415", size = 1225640, upload-time = "2025-08-10T21:25:40.489Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/362ebd3eec46c850ccf2bfe3e30f2fc4c008750011f38a850f088c56a1c6/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b", size = 1244074, upload-time = "2025-08-10T21:25:42.221Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bb/f09a1e66dab8984773d13184a10a29fe67125337649d26bdef547024ed6b/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154", size = 1293036, upload-time = "2025-08-10T21:25:43.801Z" }, + { url = "https://files.pythonhosted.org/packages/ea/01/11ecf892f201cafda0f68fa59212edaea93e96c37884b747c181303fccd1/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48", size = 2175310, upload-time = "2025-08-10T21:25:45.045Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5f/bfe11d5b934f500cc004314819ea92427e6e5462706a498c1d4fc052e08f/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220", size = 2270943, upload-time = "2025-08-10T21:25:46.393Z" }, + { url = "https://files.pythonhosted.org/packages/3d/de/259f786bf71f1e03e73d87e2db1a9a3bcab64d7b4fd780167123161630ad/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586", size = 2440488, upload-time = "2025-08-10T21:25:48.074Z" }, + { url = "https://files.pythonhosted.org/packages/1b/76/c989c278faf037c4d3421ec07a5c452cd3e09545d6dae7f87c15f54e4edf/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634", size = 2246787, upload-time = "2025-08-10T21:25:49.442Z" }, + { url = "https://files.pythonhosted.org/packages/a2/55/c2898d84ca440852e560ca9f2a0d28e6e931ac0849b896d77231929900e7/kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611", size = 73730, upload-time = "2025-08-10T21:25:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/e8/09/486d6ac523dd33b80b368247f238125d027964cfacb45c654841e88fb2ae/kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536", size = 65036, upload-time = "2025-08-10T21:25:52.063Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, + { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, + { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, + { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, + { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, + { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, + { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, + { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, + { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, + { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, + { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" }, + { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" }, + { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, + { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, + { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" }, + { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, + { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, + { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, + { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, + { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, + { url = "https://files.pythonhosted.org/packages/a2/63/fde392691690f55b38d5dd7b3710f5353bf7a8e52de93a22968801ab8978/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527", size = 60183, upload-time = "2025-08-10T21:27:37.669Z" }, + { url = "https://files.pythonhosted.org/packages/27/b1/6aad34edfdb7cced27f371866f211332bba215bfd918ad3322a58f480d8b/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771", size = 58675, upload-time = "2025-08-10T21:27:39.031Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1a/23d855a702bb35a76faed5ae2ba3de57d323f48b1f6b17ee2176c4849463/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e", size = 80277, upload-time = "2025-08-10T21:27:40.129Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5b/5239e3c2b8fb5afa1e8508f721bb77325f740ab6994d963e61b2b7abcc1e/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9", size = 77994, upload-time = "2025-08-10T21:27:41.181Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1c/5d4d468fb16f8410e596ed0eac02d2c68752aa7dc92997fe9d60a7147665/kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb", size = 73744, upload-time = "2025-08-10T21:27:42.254Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, + { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, +] + +[[package]] +name = "kloppy" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fsspec", version = "2025.10.0", source = { registry = "https://pypi.org/simple" }, extra = ["http"], marker = "python_full_version < '3.10'" }, + { name = "fsspec", version = "2026.2.0", source = { registry = "https://pypi.org/simple" }, extra = ["http"], marker = "python_full_version >= '3.10'" }, + { name = "lxml" }, + { name = "pytz" }, + { name = "sortedcontainers" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/41/d456e664badb4f289f58ffd864a33ac55650a64000a2549b45d2da207b28/kloppy-3.18.0.tar.gz", hash = "sha256:684f03fb228290592fbc2fd180f7d428aeb21d001c5f6c6d3361a21ef92fa17b", size = 224043, upload-time = "2025-10-23T08:55:23.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/79/7da8eae36417749744e1364adaaff16749aa22b6b023c8acbe8eda30bb79/kloppy-3.18.0-py3-none-any.whl", hash = "sha256:cc5087a2e8fe32a4095ba24498b5838eac8724354b99e5695b5b05c469bb63fe", size = 294701, upload-time = "2025-10-23T08:55:21.462Z" }, +] + +[[package]] +name = "librt" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/d9/6f3d3fcf5e5543ed8a60cc70fa7d50508ed60b8a10e9af6d2058159ab54e/librt-0.7.3.tar.gz", hash = "sha256:3ec50cf65235ff5c02c5b747748d9222e564ad48597122a361269dd3aa808798", size = 144549, upload-time = "2025-12-06T19:04:45.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/66/79a14e672256ef58144a24eb49adb338ec02de67ff4b45320af6504682ab/librt-0.7.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2682162855a708e3270eba4b92026b93f8257c3e65278b456c77631faf0f4f7a", size = 54707, upload-time = "2025-12-06T19:03:10.881Z" }, + { url = "https://files.pythonhosted.org/packages/58/fa/b709c65a9d5eab85f7bcfe0414504d9775aaad6e78727a0327e175474caa/librt-0.7.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:440c788f707c061d237c1e83edf6164ff19f5c0f823a3bf054e88804ebf971ec", size = 56670, upload-time = "2025-12-06T19:03:12.107Z" }, + { url = "https://files.pythonhosted.org/packages/3a/56/0685a0772ec89ddad4c00e6b584603274c3d818f9a68e2c43c4eb7b39ee9/librt-0.7.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399938edbd3d78339f797d685142dd8a623dfaded023cf451033c85955e4838a", size = 161045, upload-time = "2025-12-06T19:03:13.444Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d9/863ada0c5ce48aefb89df1555e392b2209fcb6daee4c153c031339b9a89b/librt-0.7.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1975eda520957c6e0eb52d12968dd3609ffb7eef05d4223d097893d6daf1d8a7", size = 169532, upload-time = "2025-12-06T19:03:14.699Z" }, + { url = "https://files.pythonhosted.org/packages/68/a0/71da6c8724fd16c31749905ef1c9e11de206d9301b5be984bf2682b4efb3/librt-0.7.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9da128d0edf990cf0d2ca011b02cd6f639e79286774bd5b0351245cbb5a6e51", size = 183277, upload-time = "2025-12-06T19:03:16.446Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/9c97bf2f8338ba1914de233ea312bba2bbd7c59f43f807b3e119796bab18/librt-0.7.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19acfde38cb532a560b98f473adc741c941b7a9bc90f7294bc273d08becb58b", size = 179045, upload-time = "2025-12-06T19:03:17.838Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b1/ceea067f489e904cb4ddcca3c9b06ba20229bc3fa7458711e24a5811f162/librt-0.7.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7b4f57f7a0c65821c5441d98c47ff7c01d359b1e12328219709bdd97fdd37f90", size = 173521, upload-time = "2025-12-06T19:03:19.17Z" }, + { url = "https://files.pythonhosted.org/packages/7a/41/6cb18f5da9c89ed087417abb0127a445a50ad4eaf1282ba5b52588187f47/librt-0.7.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:256793988bff98040de23c57cf36e1f4c2f2dc3dcd17537cdac031d3b681db71", size = 193592, upload-time = "2025-12-06T19:03:20.637Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3c/fcef208746584e7c78584b7aedc617130c4a4742cb8273361bbda8b183b5/librt-0.7.3-cp310-cp310-win32.whl", hash = "sha256:fcb72249ac4ea81a7baefcbff74df7029c3cb1cf01a711113fa052d563639c9c", size = 47201, upload-time = "2025-12-06T19:03:21.764Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bf/d8a6c35d1b2b789a4df9b3ddb1c8f535ea373fde2089698965a8f0d62138/librt-0.7.3-cp310-cp310-win_amd64.whl", hash = "sha256:4887c29cadbdc50640179e3861c276325ff2986791e6044f73136e6e798ff806", size = 54371, upload-time = "2025-12-06T19:03:23.231Z" }, + { url = "https://files.pythonhosted.org/packages/21/e6/f6391f5c6f158d31ed9af6bd1b1bcd3ffafdea1d816bc4219d0d90175a7f/librt-0.7.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:687403cced6a29590e6be6964463835315905221d797bc5c934a98750fe1a9af", size = 54711, upload-time = "2025-12-06T19:03:24.6Z" }, + { url = "https://files.pythonhosted.org/packages/ab/1b/53c208188c178987c081560a0fcf36f5ca500d5e21769596c845ef2f40d4/librt-0.7.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:24d70810f6e2ea853ff79338001533716b373cc0f63e2a0be5bc96129edb5fb5", size = 56664, upload-time = "2025-12-06T19:03:25.969Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5c/d9da832b9a1e5f8366e8a044ec80217945385b26cb89fd6f94bfdc7d80b0/librt-0.7.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bf8c7735fbfc0754111f00edda35cf9e98a8d478de6c47b04eaa9cef4300eaa7", size = 161701, upload-time = "2025-12-06T19:03:27.035Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/1e0a7aba15e78529dd21f233076b876ee58c8b8711b1793315bdd3b263b0/librt-0.7.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32d43610dff472eab939f4d7fbdd240d1667794192690433672ae22d7af8445", size = 171040, upload-time = "2025-12-06T19:03:28.482Z" }, + { url = "https://files.pythonhosted.org/packages/69/46/3cfa325c1c2bc25775ec6ec1718cfbec9cff4ac767d37d2d3a2d1cc6f02c/librt-0.7.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:adeaa886d607fb02563c1f625cf2ee58778a2567c0c109378da8f17ec3076ad7", size = 184720, upload-time = "2025-12-06T19:03:29.599Z" }, + { url = "https://files.pythonhosted.org/packages/99/bb/e4553433d7ac47f4c75d0a7e59b13aee0e08e88ceadbee356527a9629b0a/librt-0.7.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:572a24fc5958c61431da456a0ef1eeea6b4989d81eeb18b8e5f1f3077592200b", size = 180731, upload-time = "2025-12-06T19:03:31.201Z" }, + { url = "https://files.pythonhosted.org/packages/35/89/51cd73006232981a3106d4081fbaa584ac4e27b49bc02266468d3919db03/librt-0.7.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6488e69d408b492e08bfb68f20c4a899a354b4386a446ecd490baff8d0862720", size = 174565, upload-time = "2025-12-06T19:03:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/42/54/0578a78b587e5aa22486af34239a052c6366835b55fc307bc64380229e3f/librt-0.7.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ed028fc3d41adda916320712838aec289956c89b4f0a361ceadf83a53b4c047a", size = 195247, upload-time = "2025-12-06T19:03:34.434Z" }, + { url = "https://files.pythonhosted.org/packages/b5/0a/ee747cd999753dd9447e50b98fc36ee433b6c841a42dbf6d47b64b32a56e/librt-0.7.3-cp311-cp311-win32.whl", hash = "sha256:2cf9d73499486ce39eebbff5f42452518cc1f88d8b7ea4a711ab32962b176ee2", size = 47514, upload-time = "2025-12-06T19:03:35.959Z" }, + { url = "https://files.pythonhosted.org/packages/ec/af/8b13845178dec488e752878f8e290f8f89e7e34ae1528b70277aa1a6dd1e/librt-0.7.3-cp311-cp311-win_amd64.whl", hash = "sha256:35f1609e3484a649bb80431310ddbec81114cd86648f1d9482bc72a3b86ded2e", size = 54695, upload-time = "2025-12-06T19:03:36.956Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/ae59578501b1a25850266778f59279f4f3e726acc5c44255bfcb07b4bc57/librt-0.7.3-cp311-cp311-win_arm64.whl", hash = "sha256:550fdbfbf5bba6a2960b27376ca76d6aaa2bd4b1a06c4255edd8520c306fcfc0", size = 48142, upload-time = "2025-12-06T19:03:38.263Z" }, + { url = "https://files.pythonhosted.org/packages/29/90/ed8595fa4e35b6020317b5ea8d226a782dcbac7a997c19ae89fb07a41c66/librt-0.7.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fa9ac2e49a6bee56e47573a6786cb635e128a7b12a0dc7851090037c0d397a3", size = 55687, upload-time = "2025-12-06T19:03:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f6/6a20702a07b41006cb001a759440cb6b5362530920978f64a2b2ae2bf729/librt-0.7.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e980cf1ed1a2420a6424e2ed884629cdead291686f1048810a817de07b5eb18", size = 57127, upload-time = "2025-12-06T19:03:40.3Z" }, + { url = "https://files.pythonhosted.org/packages/79/f3/b0c4703d5ffe9359b67bb2ccb86c42d4e930a363cfc72262ac3ba53cff3e/librt-0.7.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e094e445c37c57e9ec612847812c301840239d34ccc5d153a982fa9814478c60", size = 165336, upload-time = "2025-12-06T19:03:41.369Z" }, + { url = "https://files.pythonhosted.org/packages/02/69/3ba05b73ab29ccbe003856232cea4049769be5942d799e628d1470ed1694/librt-0.7.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aca73d70c3f553552ba9133d4a09e767dcfeee352d8d8d3eb3f77e38a3beb3ed", size = 174237, upload-time = "2025-12-06T19:03:42.44Z" }, + { url = "https://files.pythonhosted.org/packages/22/ad/d7c2671e7bf6c285ef408aa435e9cd3fdc06fd994601e1f2b242df12034f/librt-0.7.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c634a0a6db395fdaba0361aa78395597ee72c3aad651b9a307a3a7eaf5efd67e", size = 189017, upload-time = "2025-12-06T19:03:44.01Z" }, + { url = "https://files.pythonhosted.org/packages/f4/94/d13f57193148004592b618555f296b41d2d79b1dc814ff8b3273a0bf1546/librt-0.7.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a59a69deeb458c858b8fea6acf9e2acd5d755d76cd81a655256bc65c20dfff5b", size = 183983, upload-time = "2025-12-06T19:03:45.834Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/b612a9944ebd39fa143c7e2e2d33f2cb790205e025ddd903fb509a3a3bb3/librt-0.7.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d91e60ac44bbe3a77a67af4a4c13114cbe9f6d540337ce22f2c9eaf7454ca71f", size = 177602, upload-time = "2025-12-06T19:03:46.944Z" }, + { url = "https://files.pythonhosted.org/packages/1f/48/77bc05c4cc232efae6c5592c0095034390992edbd5bae8d6cf1263bb7157/librt-0.7.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:703456146dc2bf430f7832fd1341adac5c893ec3c1430194fdcefba00012555c", size = 199282, upload-time = "2025-12-06T19:03:48.069Z" }, + { url = "https://files.pythonhosted.org/packages/12/aa/05916ccd864227db1ffec2a303ae34f385c6b22d4e7ce9f07054dbcf083c/librt-0.7.3-cp312-cp312-win32.whl", hash = "sha256:b7c1239b64b70be7759554ad1a86288220bbb04d68518b527783c4ad3fb4f80b", size = 47879, upload-time = "2025-12-06T19:03:49.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/92/7f41c42d31ea818b3c4b9cc1562e9714bac3c676dd18f6d5dd3d0f2aa179/librt-0.7.3-cp312-cp312-win_amd64.whl", hash = "sha256:ef59c938f72bdbc6ab52dc50f81d0637fde0f194b02d636987cea2ab30f8f55a", size = 54972, upload-time = "2025-12-06T19:03:50.335Z" }, + { url = "https://files.pythonhosted.org/packages/3f/dc/53582bbfb422311afcbc92adb75711f04e989cec052f08ec0152fbc36c9c/librt-0.7.3-cp312-cp312-win_arm64.whl", hash = "sha256:ff21c554304e8226bf80c3a7754be27c6c3549a9fec563a03c06ee8f494da8fc", size = 48338, upload-time = "2025-12-06T19:03:51.431Z" }, + { url = "https://files.pythonhosted.org/packages/93/7d/e0ce1837dfb452427db556e6d4c5301ba3b22fe8de318379fbd0593759b9/librt-0.7.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56f2a47beda8409061bc1c865bef2d4bd9ff9255219402c0817e68ab5ad89aed", size = 55742, upload-time = "2025-12-06T19:03:52.459Z" }, + { url = "https://files.pythonhosted.org/packages/be/c0/3564262301e507e1d5cf31c7d84cb12addf0d35e05ba53312494a2eba9a4/librt-0.7.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14569ac5dd38cfccf0a14597a88038fb16811a6fede25c67b79c6d50fc2c8fdc", size = 57163, upload-time = "2025-12-06T19:03:53.516Z" }, + { url = "https://files.pythonhosted.org/packages/be/ac/245e72b7e443d24a562f6047563c7f59833384053073ef9410476f68505b/librt-0.7.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6038ccbd5968325a5d6fd393cf6e00b622a8de545f0994b89dd0f748dcf3e19e", size = 165840, upload-time = "2025-12-06T19:03:54.918Z" }, + { url = "https://files.pythonhosted.org/packages/98/af/587e4491f40adba066ba39a450c66bad794c8d92094f936a201bfc7c2b5f/librt-0.7.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d39079379a9a28e74f4d57dc6357fa310a1977b51ff12239d7271ec7e71d67f5", size = 174827, upload-time = "2025-12-06T19:03:56.082Z" }, + { url = "https://files.pythonhosted.org/packages/78/21/5b8c60ea208bc83dd00421022a3874330685d7e856404128dc3728d5d1af/librt-0.7.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8837d5a52a2d7aa9f4c3220a8484013aed1d8ad75240d9a75ede63709ef89055", size = 189612, upload-time = "2025-12-06T19:03:57.507Z" }, + { url = "https://files.pythonhosted.org/packages/da/2f/8b819169ef696421fb81cd04c6cdf225f6e96f197366001e9d45180d7e9e/librt-0.7.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:399bbd7bcc1633c3e356ae274a1deb8781c7bf84d9c7962cc1ae0c6e87837292", size = 184584, upload-time = "2025-12-06T19:03:58.686Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fc/af9d225a9395b77bd7678362cb055d0b8139c2018c37665de110ca388022/librt-0.7.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8d8cf653e798ee4c4e654062b633db36984a1572f68c3aa25e364a0ddfbbb910", size = 178269, upload-time = "2025-12-06T19:03:59.769Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d8/7b4fa1683b772966749d5683aa3fd605813defffe157833a8fa69cc89207/librt-0.7.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2f03484b54bf4ae80ab2e504a8d99d20d551bfe64a7ec91e218010b467d77093", size = 199852, upload-time = "2025-12-06T19:04:00.901Z" }, + { url = "https://files.pythonhosted.org/packages/77/e8/4598413aece46ca38d9260ef6c51534bd5f34b5c21474fcf210ce3a02123/librt-0.7.3-cp313-cp313-win32.whl", hash = "sha256:44b3689b040df57f492e02cd4f0bacd1b42c5400e4b8048160c9d5e866de8abe", size = 47936, upload-time = "2025-12-06T19:04:02.054Z" }, + { url = "https://files.pythonhosted.org/packages/af/80/ac0e92d5ef8c6791b3e2c62373863827a279265e0935acdf807901353b0e/librt-0.7.3-cp313-cp313-win_amd64.whl", hash = "sha256:6b407c23f16ccc36614c136251d6b32bf30de7a57f8e782378f1107be008ddb0", size = 54965, upload-time = "2025-12-06T19:04:03.224Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fd/042f823fcbff25c1449bb4203a29919891ca74141b68d3a5f6612c4ce283/librt-0.7.3-cp313-cp313-win_arm64.whl", hash = "sha256:abfc57cab3c53c4546aee31859ef06753bfc136c9d208129bad23e2eca39155a", size = 48350, upload-time = "2025-12-06T19:04:04.234Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ae/c6ecc7bb97134a71b5241e8855d39964c0e5f4d96558f0d60593892806d2/librt-0.7.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:120dd21d46ff875e849f1aae19346223cf15656be489242fe884036b23d39e93", size = 55175, upload-time = "2025-12-06T19:04:05.308Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bc/2cc0cb0ab787b39aa5c7645cd792433c875982bdf12dccca558b89624594/librt-0.7.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1617bea5ab31266e152871208502ee943cb349c224846928a1173c864261375e", size = 56881, upload-time = "2025-12-06T19:04:06.674Z" }, + { url = "https://files.pythonhosted.org/packages/8e/87/397417a386190b70f5bf26fcedbaa1515f19dce33366e2684c6b7ee83086/librt-0.7.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93b2a1f325fefa1482516ced160c8c7b4b8d53226763fa6c93d151fa25164207", size = 163710, upload-time = "2025-12-06T19:04:08.437Z" }, + { url = "https://files.pythonhosted.org/packages/c9/37/7338f85b80e8a17525d941211451199845093ca242b32efbf01df8531e72/librt-0.7.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d4801db8354436fd3936531e7f0e4feb411f62433a6b6cb32bb416e20b529f", size = 172471, upload-time = "2025-12-06T19:04:10.124Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e0/741704edabbfae2c852fedc1b40d9ed5a783c70ed3ed8e4fe98f84b25d13/librt-0.7.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11ad45122bbed42cfc8b0597450660126ef28fd2d9ae1a219bc5af8406f95678", size = 186804, upload-time = "2025-12-06T19:04:11.586Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d1/0a82129d6ba242f3be9af34815be089f35051bc79619f5c27d2c449ecef6/librt-0.7.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6b4e7bff1d76dd2b46443078519dc75df1b5e01562345f0bb740cea5266d8218", size = 181817, upload-time = "2025-12-06T19:04:12.802Z" }, + { url = "https://files.pythonhosted.org/packages/4f/32/704f80bcf9979c68d4357c46f2af788fbf9d5edda9e7de5786ed2255e911/librt-0.7.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:d86f94743a11873317094326456b23f8a5788bad9161fd2f0e52088c33564620", size = 175602, upload-time = "2025-12-06T19:04:14.004Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6d/4355cfa0fae0c062ba72f541d13db5bc575770125a7ad3d4f46f4109d305/librt-0.7.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:754a0d09997095ad764ccef050dd5bf26cbf457aab9effcba5890dad081d879e", size = 196497, upload-time = "2025-12-06T19:04:15.487Z" }, + { url = "https://files.pythonhosted.org/packages/2e/eb/ac6d8517d44209e5a712fde46f26d0055e3e8969f24d715f70bd36056230/librt-0.7.3-cp314-cp314-win32.whl", hash = "sha256:fbd7351d43b80d9c64c3cfcb50008f786cc82cba0450e8599fdd64f264320bd3", size = 44678, upload-time = "2025-12-06T19:04:16.688Z" }, + { url = "https://files.pythonhosted.org/packages/e9/93/238f026d141faf9958da588c761a0812a1a21c98cc54a76f3608454e4e59/librt-0.7.3-cp314-cp314-win_amd64.whl", hash = "sha256:d376a35c6561e81d2590506804b428fc1075fcc6298fc5bb49b771534c0ba010", size = 51689, upload-time = "2025-12-06T19:04:17.726Z" }, + { url = "https://files.pythonhosted.org/packages/52/44/43f462ad9dcf9ed7d3172fe2e30d77b980956250bd90e9889a9cca93df2a/librt-0.7.3-cp314-cp314-win_arm64.whl", hash = "sha256:cbdb3f337c88b43c3b49ca377731912c101178be91cb5071aac48faa898e6f8e", size = 44662, upload-time = "2025-12-06T19:04:18.771Z" }, + { url = "https://files.pythonhosted.org/packages/1d/35/fed6348915f96b7323241de97f26e2af481e95183b34991df12fd5ce31b1/librt-0.7.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9f0e0927efe87cd42ad600628e595a1a0aa1c64f6d0b55f7e6059079a428641a", size = 57347, upload-time = "2025-12-06T19:04:19.812Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f2/045383ccc83e3fea4fba1b761796584bc26817b6b2efb6b8a6731431d16f/librt-0.7.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:020c6db391268bcc8ce75105cb572df8cb659a43fd347366aaa407c366e5117a", size = 59223, upload-time = "2025-12-06T19:04:20.862Z" }, + { url = "https://files.pythonhosted.org/packages/77/3f/c081f8455ab1d7f4a10dbe58463ff97119272ff32494f21839c3b9029c2c/librt-0.7.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7af7785f5edd1f418da09a8cdb9ec84b0213e23d597413e06525340bcce1ea4f", size = 183861, upload-time = "2025-12-06T19:04:21.963Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f5/73c5093c22c31fbeaebc25168837f05ebfd8bf26ce00855ef97a5308f36f/librt-0.7.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ccadf260bb46a61b9c7e89e2218f6efea9f3eeaaab4e3d1f58571890e54858e", size = 194594, upload-time = "2025-12-06T19:04:23.14Z" }, + { url = "https://files.pythonhosted.org/packages/78/b8/d5f17d4afe16612a4a94abfded94c16c5a033f183074fb130dfe56fc1a42/librt-0.7.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9883b2d819ce83f87ba82a746c81d14ada78784db431e57cc9719179847376e", size = 206759, upload-time = "2025-12-06T19:04:24.328Z" }, + { url = "https://files.pythonhosted.org/packages/36/2e/021765c1be85ee23ffd5b5b968bb4cba7526a4db2a0fc27dcafbdfc32da7/librt-0.7.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:59cb0470612d21fa1efddfa0dd710756b50d9c7fb6c1236bbf8ef8529331dc70", size = 203210, upload-time = "2025-12-06T19:04:25.544Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/9923656e42da4fd18c594bd08cf6d7e152d4158f8b808e210d967f0dcceb/librt-0.7.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:1fe603877e1865b5fd047a5e40379509a4a60204aa7aa0f72b16f7a41c3f0712", size = 196708, upload-time = "2025-12-06T19:04:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/fc/0b/0708b886ac760e64d6fbe7e16024e4be3ad1a3629d19489a97e9cf4c3431/librt-0.7.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5460d99ed30f043595bbdc888f542bad2caeb6226b01c33cda3ae444e8f82d42", size = 217212, upload-time = "2025-12-06T19:04:27.892Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7f/12a73ff17bca4351e73d585dd9ebf46723c4a8622c4af7fe11a2e2d011ff/librt-0.7.3-cp314-cp314t-win32.whl", hash = "sha256:d09f677693328503c9e492e33e9601464297c01f9ebd966ea8fc5308f3069bfd", size = 45586, upload-time = "2025-12-06T19:04:29.116Z" }, + { url = "https://files.pythonhosted.org/packages/e2/df/8decd032ac9b995e4f5606cde783711a71094128d88d97a52e397daf2c89/librt-0.7.3-cp314-cp314t-win_amd64.whl", hash = "sha256:25711f364c64cab2c910a0247e90b51421e45dbc8910ceeb4eac97a9e132fc6f", size = 53002, upload-time = "2025-12-06T19:04:30.173Z" }, + { url = "https://files.pythonhosted.org/packages/de/0c/6605b6199de8178afe7efc77ca1d8e6db00453bc1d3349d27605c0f42104/librt-0.7.3-cp314-cp314t-win_arm64.whl", hash = "sha256:a9f9b661f82693eb56beb0605156c7fca57f535704ab91837405913417d6990b", size = 45647, upload-time = "2025-12-06T19:04:31.302Z" }, + { url = "https://files.pythonhosted.org/packages/e1/70/b3f19e3bb34f44e218c8271dc0b2b14eb6b183fbccbececf94c71e2b5e69/librt-0.7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cd8551aa21df6c60baa2624fd086ae7486bdde00c44097b32e1d1b1966e365e0", size = 54850, upload-time = "2025-12-06T19:04:32.742Z" }, + { url = "https://files.pythonhosted.org/packages/a0/97/6599ed7726aaa9b5bacea206d5861b94e76866240e2f394a59594bf3db46/librt-0.7.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6eb9295c730e26b849ed1f4022735f36863eb46b14b6e10604c1c39b8b5efaea", size = 56797, upload-time = "2025-12-06T19:04:34.193Z" }, + { url = "https://files.pythonhosted.org/packages/33/83/216db13224a6f688787f456909bbc50f9d951c0f4bea8ba38a2eb931d581/librt-0.7.3-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3edbf257c40d21a42615e9e332a6b10a8bacaaf58250aed8552a14a70efd0d65", size = 159681, upload-time = "2025-12-06T19:04:35.554Z" }, + { url = "https://files.pythonhosted.org/packages/83/23/0a490c8ba3bc90090647ac7b9b3c63c16af7378bcabe3ff4c7d7890d66e5/librt-0.7.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b29e97273bd6999e2bfe9fe3531b1f4f64effd28327bced048a33e49b99674a", size = 168505, upload-time = "2025-12-06T19:04:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/5e/16/b47c60805285caa06728d61d933fdd6db5b7321f375ce496cb7fdbeb1a44/librt-0.7.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e40520c37926166c24d0c2e0f3bc3a5f46646c34bdf7b4ea9747c297d6ee809", size = 182234, upload-time = "2025-12-06T19:04:37.889Z" }, + { url = "https://files.pythonhosted.org/packages/2d/2f/bef211d7f0d55fa2484d2c644b2cdae8c9c5eec050754b0516e6582ad452/librt-0.7.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6bdd9adfca615903578d2060ee8a6eb1c24eaf54919ff0ddc820118e5718931b", size = 178276, upload-time = "2025-12-06T19:04:39.408Z" }, + { url = "https://files.pythonhosted.org/packages/3d/dd/5a3e7762b086b62fabb31fd4deaaf3ba888cfdd3b8f2e3247f076c18a6ff/librt-0.7.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f57aca20e637750a2c18d979f7096e2c2033cc40cf7ed201494318de1182f135", size = 172602, upload-time = "2025-12-06T19:04:40.619Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d8/533d5bfd5b377eb03ed54101814b530fc1f9bbe0e79971c641a3f15bfb33/librt-0.7.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cad9971881e4fec00d96af7eaf4b63aa7a595696fc221808b0d3ce7ca9743258", size = 192741, upload-time = "2025-12-06T19:04:41.738Z" }, + { url = "https://files.pythonhosted.org/packages/9f/69/0b87ce8e95f65ebc864f390f1139b8fe9fac6fb64b797307447b1719610c/librt-0.7.3-cp39-cp39-win32.whl", hash = "sha256:170cdb8436188347af17bf9cccf3249ba581c933ed56d926497119d4cf730cec", size = 47154, upload-time = "2025-12-06T19:04:42.96Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1c/070dee0add2d6e742be4d8b965d5a37c24562b43e8ef7deba8ed5b5d3c0f/librt-0.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:b278a9248a4e3260fee3db7613772ca9ab6763a129d6d6f29555e2f9b168216d", size = 54339, upload-time = "2025-12-06T19:04:44.415Z" }, +] + +[[package]] +name = "lxml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/8a/f8192a08237ef2fb1b19733f709db88a4c43bc8ab8357f01cb41a27e7f6a/lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388", size = 8590589, upload-time = "2025-09-22T04:00:10.51Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/27bcd07ae17ff5e5536e8d88f4c7d581b48963817a13de11f3ac3329bfa2/lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153", size = 4629671, upload-time = "2025-09-22T04:00:15.411Z" }, + { url = "https://files.pythonhosted.org/packages/02/5a/a7d53b3291c324e0b6e48f3c797be63836cc52156ddf8f33cd72aac78866/lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31", size = 4999961, upload-time = "2025-09-22T04:00:17.619Z" }, + { url = "https://files.pythonhosted.org/packages/f5/55/d465e9b89df1761674d8672bb3e4ae2c47033b01ec243964b6e334c6743f/lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9", size = 5157087, upload-time = "2025-09-22T04:00:19.868Z" }, + { url = "https://files.pythonhosted.org/packages/62/38/3073cd7e3e8dfc3ba3c3a139e33bee3a82de2bfb0925714351ad3d255c13/lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8", size = 5067620, upload-time = "2025-09-22T04:00:21.877Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d3/1e001588c5e2205637b08985597827d3827dbaaece16348c8822bfe61c29/lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba", size = 5406664, upload-time = "2025-09-22T04:00:23.714Z" }, + { url = "https://files.pythonhosted.org/packages/20/cf/cab09478699b003857ed6ebfe95e9fb9fa3d3c25f1353b905c9b73cfb624/lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c", size = 5289397, upload-time = "2025-09-22T04:00:25.544Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/02a2d0c38ac9a8b9f9e5e1bbd3f24b3f426044ad618b552e9549ee91bd63/lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c", size = 4772178, upload-time = "2025-09-22T04:00:27.602Z" }, + { url = "https://files.pythonhosted.org/packages/56/87/e1ceadcc031ec4aa605fe95476892d0b0ba3b7f8c7dcdf88fdeff59a9c86/lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321", size = 5358148, upload-time = "2025-09-22T04:00:29.323Z" }, + { url = "https://files.pythonhosted.org/packages/fe/13/5bb6cf42bb228353fd4ac5f162c6a84fd68a4d6f67c1031c8cf97e131fc6/lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1", size = 5112035, upload-time = "2025-09-22T04:00:31.061Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e2/ea0498552102e59834e297c5c6dff8d8ded3db72ed5e8aad77871476f073/lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34", size = 4799111, upload-time = "2025-09-22T04:00:33.11Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9e/8de42b52a73abb8af86c66c969b3b4c2a96567b6ac74637c037d2e3baa60/lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a", size = 5351662, upload-time = "2025-09-22T04:00:35.237Z" }, + { url = "https://files.pythonhosted.org/packages/28/a2/de776a573dfb15114509a37351937c367530865edb10a90189d0b4b9b70a/lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c", size = 5314973, upload-time = "2025-09-22T04:00:37.086Z" }, + { url = "https://files.pythonhosted.org/packages/50/a0/3ae1b1f8964c271b5eec91db2043cf8c6c0bce101ebb2a633b51b044db6c/lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b", size = 3611953, upload-time = "2025-09-22T04:00:39.224Z" }, + { url = "https://files.pythonhosted.org/packages/d1/70/bd42491f0634aad41bdfc1e46f5cff98825fb6185688dc82baa35d509f1a/lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0", size = 4032695, upload-time = "2025-09-22T04:00:41.402Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d0/05c6a72299f54c2c561a6c6cbb2f512e047fca20ea97a05e57931f194ac4/lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5", size = 3680051, upload-time = "2025-09-22T04:00:43.525Z" }, + { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" }, + { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" }, + { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" }, + { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" }, + { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" }, + { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, + { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, + { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, + { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, + { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, + { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, + { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, + { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, + { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, + { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/38/66/dd13c74fad495957374c8a81c932f4874d3dca5aa0db9e4369f06a399718/lxml-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2c8458c2cdd29589a8367c09c8f030f1d202be673f0ca224ec18590b3b9fb694", size = 8602363, upload-time = "2025-09-22T04:03:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/5e/f4/edb9d47dce464b5dd044d35775ee794364935b93ab6226c95e199118890d/lxml-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3fee0851639d06276e6b387f1c190eb9d7f06f7f53514e966b26bae46481ec90", size = 4634995, upload-time = "2025-09-22T04:04:01.122Z" }, + { url = "https://files.pythonhosted.org/packages/66/f2/d80c97b6ed83a99bc24b2b29919d5e618af5322df6d3aa61064093712309/lxml-6.0.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b2142a376b40b6736dfc214fd2902409e9e3857eff554fed2d3c60f097e62a62", size = 5003737, upload-time = "2025-09-22T04:04:02.98Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f1/18b750f79f8889b9109b24749f23ac137870b4f685edc4be54be0ff2c730/lxml-6.0.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6b5b39cc7e2998f968f05309e666103b53e2edd01df8dc51b90d734c0825444", size = 5160821, upload-time = "2025-09-22T04:04:04.854Z" }, + { url = "https://files.pythonhosted.org/packages/cf/88/2b6a415dbad411c3e9c092128eb7db06054d2d9460aa56676d17ee4f4fd5/lxml-6.0.2-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4aec24d6b72ee457ec665344a29acb2d35937d5192faebe429ea02633151aad", size = 5070959, upload-time = "2025-09-22T04:04:07.042Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d0/5354afaa0f2e53625e5f96f6bd049a4875c3ab79d96d6c4871dd1f4a98c4/lxml-6.0.2-cp39-cp39-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:b42f4d86b451c2f9d06ffb4f8bbc776e04df3ba070b9fe2657804b1b40277c48", size = 5410267, upload-time = "2025-09-22T04:04:10.458Z" }, + { url = "https://files.pythonhosted.org/packages/51/63/10dea35a01291dc529fa9d6ba204ea627a1c77b7fbb180d404f6cc4dd2fd/lxml-6.0.2-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cdaefac66e8b8f30e37a9b4768a391e1f8a16a7526d5bc77a7928408ef68e93", size = 5292990, upload-time = "2025-09-22T04:04:12.405Z" }, + { url = "https://files.pythonhosted.org/packages/37/58/51ef422d8bec58db600b3552e5f2d870ec01ffacf11d98689c42ffdcbf7f/lxml-6.0.2-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:b738f7e648735714bbb82bdfd030203360cfeab7f6e8a34772b3c8c8b820568c", size = 4776318, upload-time = "2025-09-22T04:04:14.22Z" }, + { url = "https://files.pythonhosted.org/packages/77/97/3f797820e82e3a58a19bc51068b40f3b9ab7d0934ba6e5ba6b147b618319/lxml-6.0.2-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daf42de090d59db025af61ce6bdb2521f0f102ea0e6ea310f13c17610a97da4c", size = 5360191, upload-time = "2025-09-22T04:04:16.236Z" }, + { url = "https://files.pythonhosted.org/packages/e2/14/a9306a8ab122e2f5dfbf4f71fb09beeadca26b0c275708432bbc33f40edc/lxml-6.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:66328dabea70b5ba7e53d94aa774b733cf66686535f3bc9250a7aab53a91caaf", size = 5116114, upload-time = "2025-09-22T04:04:18.594Z" }, + { url = "https://files.pythonhosted.org/packages/ea/23/2118a1685277b9fa8726ec7ee903db55aa300dcea3d406a220cbe3710953/lxml-6.0.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:e237b807d68a61fc3b1e845407e27e5eb8ef69bc93fe8505337c1acb4ee300b6", size = 4801704, upload-time = "2025-09-22T04:04:20.466Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e8/d5be34da2059dc9a4ff8643fd6ad3f8234a27b2a44831b7fff58c4dbb3e3/lxml-6.0.2-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:ac02dc29fd397608f8eb15ac1610ae2f2f0154b03f631e6d724d9e2ad4ee2c84", size = 5355451, upload-time = "2025-09-22T04:04:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/61/84/5aebc8e150d5bf488815ea2d8798c7ff509cc37b5725caa3c1f11bdd3245/lxml-6.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:817ef43a0c0b4a77bd166dc9a09a555394105ff3374777ad41f453526e37f9cb", size = 5318630, upload-time = "2025-09-22T04:04:24.301Z" }, + { url = "https://files.pythonhosted.org/packages/35/04/629ae603c1c17fb7adc9df2bc21aa5ac96afb84001700b13c1f038f3118c/lxml-6.0.2-cp39-cp39-win32.whl", hash = "sha256:bc532422ff26b304cfb62b328826bd995c96154ffd2bac4544f37dbb95ecaa8f", size = 3614032, upload-time = "2025-09-22T04:04:26.158Z" }, + { url = "https://files.pythonhosted.org/packages/71/de/07b7b1249acbecbf48f7e42c3ce87a657af6ff38e30f12a1ad81f16010f2/lxml-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:995e783eb0374c120f528f807443ad5a83a656a8624c467ea73781fc5f8a8304", size = 4035311, upload-time = "2025-09-22T04:04:28.413Z" }, + { url = "https://files.pythonhosted.org/packages/60/e3/02c4c55b281606f3c8e118300e16a9fcf5f3462cc46ce740ed0b82fc3f1b/lxml-6.0.2-cp39-cp39-win_arm64.whl", hash = "sha256:08b9d5e803c2e4725ae9e8559ee880e5328ed61aa0935244e0515d7d9dbec0aa", size = 3683462, upload-time = "2025-09-22T04:04:30.399Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9c/780c9a8fce3f04690b374f72f41306866b0400b9d0fdf3e17aaa37887eed/lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6", size = 3939264, upload-time = "2025-09-22T04:04:32.892Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5a/1ab260c00adf645d8bf7dec7f920f744b032f69130c681302821d5debea6/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba", size = 4216435, upload-time = "2025-09-22T04:04:34.907Z" }, + { url = "https://files.pythonhosted.org/packages/f2/37/565f3b3d7ffede22874b6d86be1a1763d00f4ea9fc5b9b6ccb11e4ec8612/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5", size = 4325913, upload-time = "2025-09-22T04:04:37.205Z" }, + { url = "https://files.pythonhosted.org/packages/22/ec/f3a1b169b2fb9d03467e2e3c0c752ea30e993be440a068b125fc7dd248b0/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4", size = 4269357, upload-time = "2025-09-22T04:04:39.322Z" }, + { url = "https://files.pythonhosted.org/packages/77/a2/585a28fe3e67daa1cf2f06f34490d556d121c25d500b10082a7db96e3bcd/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d", size = 4412295, upload-time = "2025-09-22T04:04:41.647Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/a57dd8bcebd7c69386c20263830d4fa72d27e6b72a229ef7a48e88952d9a/lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d", size = 3516913, upload-time = "2025-09-22T04:04:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" }, + { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" }, + { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "0.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/41/bae1254e0396c0cc8cf1751cb7d9afc90a602353695af5952530482c963f/MarkupSafe-0.23.tar.gz", hash = "sha256:a4ec1aff59b95a14b45eb2e23761a0179e98319da5a7eb76b56ea8cdc7b871c3", size = 13416, upload-time = "2014-05-08T14:58:53.945Z" } + +[[package]] +name = "matplotlib" +version = "3.9.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "contourpy", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "cycler", marker = "python_full_version < '3.10'" }, + { name = "fonttools", version = "4.60.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "importlib-resources", marker = "python_full_version < '3.10'" }, + { name = "kiwisolver", version = "1.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyparsing", marker = "python_full_version < '3.10'" }, + { name = "python-dateutil", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/17/1747b4154034befd0ed33b52538f5eb7752d05bb51c5e2a31470c3bc7d52/matplotlib-3.9.4.tar.gz", hash = "sha256:1e00e8be7393cbdc6fedfa8a6fba02cf3e83814b285db1c60b906a023ba41bc3", size = 36106529, upload-time = "2024-12-13T05:56:34.184Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/94/27d2e2c30d54b56c7b764acc1874a909e34d1965a427fc7092bb6a588b63/matplotlib-3.9.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fdd7abfb706dfa8d307af64a87f1a862879ec3cd8d0ec8637458f0885b9c50", size = 7885089, upload-time = "2024-12-13T05:54:24.224Z" }, + { url = "https://files.pythonhosted.org/packages/c6/25/828273307e40a68eb8e9df832b6b2aaad075864fdc1de4b1b81e40b09e48/matplotlib-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d89bc4e85e40a71d1477780366c27fb7c6494d293e1617788986f74e2a03d7ff", size = 7770600, upload-time = "2024-12-13T05:54:27.214Z" }, + { url = "https://files.pythonhosted.org/packages/f2/65/f841a422ec994da5123368d76b126acf4fc02ea7459b6e37c4891b555b83/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddf9f3c26aae695c5daafbf6b94e4c1a30d6cd617ba594bbbded3b33a1fcfa26", size = 8200138, upload-time = "2024-12-13T05:54:29.497Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/272aca07a38804d93b6050813de41ca7ab0e29ba7a9dd098e12037c919a9/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18ebcf248030173b59a868fda1fe42397253f6698995b55e81e1f57431d85e50", size = 8312711, upload-time = "2024-12-13T05:54:34.396Z" }, + { url = "https://files.pythonhosted.org/packages/98/37/f13e23b233c526b7e27ad61be0a771894a079e0f7494a10d8d81557e0e9a/matplotlib-3.9.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974896ec43c672ec23f3f8c648981e8bc880ee163146e0312a9b8def2fac66f5", size = 9090622, upload-time = "2024-12-13T05:54:36.808Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8c/b1f5bd2bd70e60f93b1b54c4d5ba7a992312021d0ddddf572f9a1a6d9348/matplotlib-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:4598c394ae9711cec135639374e70871fa36b56afae17bdf032a345be552a88d", size = 7828211, upload-time = "2024-12-13T05:54:40.596Z" }, + { url = "https://files.pythonhosted.org/packages/74/4b/65be7959a8fa118a3929b49a842de5b78bb55475236fcf64f3e308ff74a0/matplotlib-3.9.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4dd29641d9fb8bc4492420c5480398dd40a09afd73aebe4eb9d0071a05fbe0c", size = 7894430, upload-time = "2024-12-13T05:54:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/e9/18/80f70d91896e0a517b4a051c3fd540daa131630fd75e02e250365353b253/matplotlib-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30e5b22e8bcfb95442bf7d48b0d7f3bdf4a450cbf68986ea45fca3d11ae9d099", size = 7780045, upload-time = "2024-12-13T05:54:46.414Z" }, + { url = "https://files.pythonhosted.org/packages/a2/73/ccb381026e3238c5c25c3609ba4157b2d1a617ec98d65a8b4ee4e1e74d02/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bb0030d1d447fd56dcc23b4c64a26e44e898f0416276cac1ebc25522e0ac249", size = 8209906, upload-time = "2024-12-13T05:54:49.459Z" }, + { url = "https://files.pythonhosted.org/packages/ab/33/1648da77b74741c89f5ea95cbf42a291b4b364f2660b316318811404ed97/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aca90ed222ac3565d2752b83dbb27627480d27662671e4d39da72e97f657a423", size = 8322873, upload-time = "2024-12-13T05:54:53.066Z" }, + { url = "https://files.pythonhosted.org/packages/57/d3/8447ba78bc6593c9044c372d1609f8ea10fb1e071e7a9e0747bea74fc16c/matplotlib-3.9.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a181b2aa2906c608fcae72f977a4a2d76e385578939891b91c2550c39ecf361e", size = 9099566, upload-time = "2024-12-13T05:54:55.522Z" }, + { url = "https://files.pythonhosted.org/packages/23/e1/4f0e237bf349c02ff9d1b6e7109f1a17f745263809b9714a8576dc17752b/matplotlib-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:1f6882828231eca17f501c4dcd98a05abb3f03d157fbc0769c6911fe08b6cfd3", size = 7838065, upload-time = "2024-12-13T05:54:58.337Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2b/c918bf6c19d6445d1cefe3d2e42cb740fb997e14ab19d4daeb6a7ab8a157/matplotlib-3.9.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dfc48d67e6661378a21c2983200a654b72b5c5cdbd5d2cf6e5e1ece860f0cc70", size = 7891131, upload-time = "2024-12-13T05:55:02.837Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e5/b4e8fc601ca302afeeabf45f30e706a445c7979a180e3a978b78b2b681a4/matplotlib-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47aef0fab8332d02d68e786eba8113ffd6f862182ea2999379dec9e237b7e483", size = 7776365, upload-time = "2024-12-13T05:55:05.158Z" }, + { url = "https://files.pythonhosted.org/packages/99/06/b991886c506506476e5d83625c5970c656a491b9f80161458fed94597808/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fba1f52c6b7dc764097f52fd9ab627b90db452c9feb653a59945de16752e965f", size = 8200707, upload-time = "2024-12-13T05:55:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e2/556b627498cb27e61026f2d1ba86a78ad1b836fef0996bef5440e8bc9559/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:173ac3748acaac21afcc3fa1633924609ba1b87749006bc25051c52c422a5d00", size = 8313761, upload-time = "2024-12-13T05:55:12.95Z" }, + { url = "https://files.pythonhosted.org/packages/58/ff/165af33ec766ff818306ea88e91f9f60d2a6ed543be1eb122a98acbf3b0d/matplotlib-3.9.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320edea0cadc07007765e33f878b13b3738ffa9745c5f707705692df70ffe0e0", size = 9095284, upload-time = "2024-12-13T05:55:16.199Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8b/3d0c7a002db3b1ed702731c2a9a06d78d035f1f2fb0fb936a8e43cc1e9f4/matplotlib-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a4a4cfc82330b27042a7169533da7991e8789d180dd5b3daeaee57d75cd5a03b", size = 7841160, upload-time = "2024-12-13T05:55:19.991Z" }, + { url = "https://files.pythonhosted.org/packages/49/b1/999f89a7556d101b23a2f0b54f1b6e140d73f56804da1398f2f0bc0924bc/matplotlib-3.9.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37eeffeeca3c940985b80f5b9a7b95ea35671e0e7405001f249848d2b62351b6", size = 7891499, upload-time = "2024-12-13T05:55:22.142Z" }, + { url = "https://files.pythonhosted.org/packages/87/7b/06a32b13a684977653396a1bfcd34d4e7539c5d55c8cbfaa8ae04d47e4a9/matplotlib-3.9.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3e7465ac859ee4abcb0d836137cd8414e7bb7ad330d905abced457217d4f0f45", size = 7776802, upload-time = "2024-12-13T05:55:25.947Z" }, + { url = "https://files.pythonhosted.org/packages/65/87/ac498451aff739e515891bbb92e566f3c7ef31891aaa878402a71f9b0910/matplotlib-3.9.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4c12302c34afa0cf061bea23b331e747e5e554b0fa595c96e01c7b75bc3b858", size = 8200802, upload-time = "2024-12-13T05:55:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6b/9eb761c00e1cb838f6c92e5f25dcda3f56a87a52f6cb8fdfa561e6cf6a13/matplotlib-3.9.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b8c97917f21b75e72108b97707ba3d48f171541a74aa2a56df7a40626bafc64", size = 8313880, upload-time = "2024-12-13T05:55:30.965Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a2/c8eaa600e2085eec7e38cbbcc58a30fc78f8224939d31d3152bdafc01fd1/matplotlib-3.9.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0229803bd7e19271b03cb09f27db76c918c467aa4ce2ae168171bc67c3f508df", size = 9094637, upload-time = "2024-12-13T05:55:33.701Z" }, + { url = "https://files.pythonhosted.org/packages/71/1f/c6e1daea55b7bfeb3d84c6cb1abc449f6a02b181e7e2a5e4db34c3afb793/matplotlib-3.9.4-cp313-cp313-win_amd64.whl", hash = "sha256:7c0d8ef442ebf56ff5e206f8083d08252ee738e04f3dc88ea882853a05488799", size = 7841311, upload-time = "2024-12-13T05:55:36.737Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3a/2757d3f7d388b14dd48f5a83bea65b6d69f000e86b8f28f74d86e0d375bd/matplotlib-3.9.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a04c3b00066a688834356d196136349cb32f5e1003c55ac419e91585168b88fb", size = 7919989, upload-time = "2024-12-13T05:55:39.024Z" }, + { url = "https://files.pythonhosted.org/packages/24/28/f5077c79a4f521589a37fe1062d6a6ea3534e068213f7357e7cfffc2e17a/matplotlib-3.9.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04c519587f6c210626741a1e9a68eefc05966ede24205db8982841826af5871a", size = 7809417, upload-time = "2024-12-13T05:55:42.412Z" }, + { url = "https://files.pythonhosted.org/packages/36/c8/c523fd2963156692916a8eb7d4069084cf729359f7955cf09075deddfeaf/matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308afbf1a228b8b525fcd5cec17f246bbbb63b175a3ef6eb7b4d33287ca0cf0c", size = 8226258, upload-time = "2024-12-13T05:55:47.259Z" }, + { url = "https://files.pythonhosted.org/packages/f6/88/499bf4b8fa9349b6f5c0cf4cead0ebe5da9d67769129f1b5651e5ac51fbc/matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb3b02246ddcffd3ce98e88fed5b238bc5faff10dbbaa42090ea13241d15764", size = 8335849, upload-time = "2024-12-13T05:55:49.763Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9f/20a4156b9726188646a030774ee337d5ff695a965be45ce4dbcb9312c170/matplotlib-3.9.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8a75287e9cb9eee48cb79ec1d806f75b29c0fde978cb7223a1f4c5848d696041", size = 9102152, upload-time = "2024-12-13T05:55:51.997Z" }, + { url = "https://files.pythonhosted.org/packages/10/11/237f9c3a4e8d810b1759b67ff2da7c32c04f9c80aa475e7beb36ed43a8fb/matplotlib-3.9.4-cp313-cp313t-win_amd64.whl", hash = "sha256:488deb7af140f0ba86da003e66e10d55ff915e152c78b4b66d231638400b1965", size = 7896987, upload-time = "2024-12-13T05:55:55.941Z" }, + { url = "https://files.pythonhosted.org/packages/56/eb/501b465c9fef28f158e414ea3a417913dc2ac748564c7ed41535f23445b4/matplotlib-3.9.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3c3724d89a387ddf78ff88d2a30ca78ac2b4c89cf37f2db4bd453c34799e933c", size = 7885919, upload-time = "2024-12-13T05:55:59.66Z" }, + { url = "https://files.pythonhosted.org/packages/da/36/236fbd868b6c91309a5206bd90c3f881f4f44b2d997cd1d6239ef652f878/matplotlib-3.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d5f0a8430ffe23d7e32cfd86445864ccad141797f7d25b7c41759a5b5d17cfd7", size = 7771486, upload-time = "2024-12-13T05:56:04.264Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4b/105caf2d54d5ed11d9f4335398f5103001a03515f2126c936a752ccf1461/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bb0141a21aef3b64b633dc4d16cbd5fc538b727e4958be82a0e1c92a234160e", size = 8201838, upload-time = "2024-12-13T05:56:06.792Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a7/bb01188fb4013d34d274caf44a2f8091255b0497438e8b6c0a7c1710c692/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57aa235109e9eed52e2c2949db17da185383fa71083c00c6c143a60e07e0888c", size = 8314492, upload-time = "2024-12-13T05:56:09.964Z" }, + { url = "https://files.pythonhosted.org/packages/33/19/02e1a37f7141fc605b193e927d0a9cdf9dc124a20b9e68793f4ffea19695/matplotlib-3.9.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b18c600061477ccfdd1e6fd050c33d8be82431700f3452b297a56d9ed7037abb", size = 9092500, upload-time = "2024-12-13T05:56:13.55Z" }, + { url = "https://files.pythonhosted.org/packages/57/68/c2feb4667adbf882ffa4b3e0ac9967f848980d9f8b5bebd86644aa67ce6a/matplotlib-3.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:ef5f2d1b67d2d2145ff75e10f8c008bfbf71d45137c4b648c87193e7dd053eac", size = 7822962, upload-time = "2024-12-13T05:56:16.358Z" }, + { url = "https://files.pythonhosted.org/packages/0c/22/2ef6a364cd3f565442b0b055e0599744f1e4314ec7326cdaaa48a4d864d7/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:44e0ed786d769d85bc787b0606a53f2d8d2d1d3c8a2608237365e9121c1a338c", size = 7877995, upload-time = "2024-12-13T05:56:18.805Z" }, + { url = "https://files.pythonhosted.org/packages/87/b8/2737456e566e9f4d94ae76b8aa0d953d9acb847714f9a7ad80184474f5be/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:09debb9ce941eb23ecdbe7eab972b1c3e0276dcf01688073faff7b0f61d6c6ca", size = 7769300, upload-time = "2024-12-13T05:56:21.315Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1f/e709c6ec7b5321e6568769baa288c7178e60a93a9da9e682b39450da0e29/matplotlib-3.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc53cf157a657bfd03afab14774d54ba73aa84d42cfe2480c91bd94873952db", size = 8313423, upload-time = "2024-12-13T05:56:26.719Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b6/5a1f868782cd13f053a679984e222007ecff654a9bfbac6b27a65f4eeb05/matplotlib-3.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ad45da51be7ad02387801fd154ef74d942f49fe3fcd26a64c94842ba7ec0d865", size = 7854624, upload-time = "2024-12-13T05:56:29.359Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "cycler", marker = "python_full_version >= '3.10'" }, + { name = "fonttools", version = "4.61.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "kiwisolver", version = "1.4.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pillow", version = "12.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyparsing", marker = "python_full_version >= '3.10'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/be/a30bd917018ad220c400169fba298f2bb7003c8ccbc0c3e24ae2aacad1e8/matplotlib-3.10.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7", size = 8239828, upload-time = "2025-12-10T22:55:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/ca01e043c4841078e82cf6e80a6993dfecd315c3d79f5f3153afbb8e1ec6/matplotlib-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656", size = 8128050, upload-time = "2025-12-10T22:55:04.997Z" }, + { url = "https://files.pythonhosted.org/packages/cb/aa/7ab67f2b729ae6a91bcf9dcac0affb95fb8c56f7fd2b2af894ae0b0cf6fa/matplotlib-3.10.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df", size = 8700452, upload-time = "2025-12-10T22:55:07.47Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/2d5817b0acee3c49b7e7ccfbf5b273f284957cc8e270adf36375db353190/matplotlib-3.10.8-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17", size = 9534928, upload-time = "2025-12-10T22:55:10.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5b/8e66653e9f7c39cb2e5cab25fce4810daffa2bff02cbf5f3077cea9e942c/matplotlib-3.10.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933", size = 9586377, upload-time = "2025-12-10T22:55:12.362Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/fd0bbadf837f81edb0d208ba8f8cb552874c3b16e27cb91a31977d90875d/matplotlib-3.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a", size = 8128127, upload-time = "2025-12-10T22:55:14.436Z" }, + { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, + { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, + { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, + { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, + { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/f5/43/31d59500bb950b0d188e149a2e552040528c13d6e3d6e84d0cccac593dcd/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8", size = 8237252, upload-time = "2025-12-10T22:56:39.529Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2c/615c09984f3c5f907f51c886538ad785cf72e0e11a3225de2c0f9442aecc/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7", size = 8124693, upload-time = "2025-12-10T22:56:41.758Z" }, + { url = "https://files.pythonhosted.org/packages/91/e1/2757277a1c56041e1fc104b51a0f7b9a4afc8eb737865d63cababe30bc61/matplotlib-3.10.8-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3", size = 8702205, upload-time = "2025-12-10T22:56:43.415Z" }, + { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, + { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, +] + +[[package]] +name = "maturin" +version = "1.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/44/c593afce7d418ae6016b955c978055232359ad28c707a9ac6643fc60512d/maturin-1.10.2.tar.gz", hash = "sha256:259292563da89850bf8f7d37aa4ddba22905214c1e180b1c8f55505dfd8c0e81", size = 217835, upload-time = "2025-11-19T11:53:17.348Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/74/7f7e93019bb71aa072a7cdf951cbe4c9a8d5870dd86c66ec67002153487f/maturin-1.10.2-py3-none-linux_armv6l.whl", hash = "sha256:11c73815f21a755d2129c410e6cb19dbfacbc0155bfc46c706b69930c2eb794b", size = 8763201, upload-time = "2025-11-19T11:52:42.98Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/1d1b64dbb6518ee633bfde8787e251ae59428818fea7a6bdacb8008a09bd/maturin-1.10.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7fbd997c5347649ee7987bd05a92bd5b8b07efa4ac3f8bcbf6196e07eb573d89", size = 17072583, upload-time = "2025-11-19T11:52:45.636Z" }, + { url = "https://files.pythonhosted.org/packages/7c/45/2418f0d6e1cbdf890205d1dc73ebea6778bb9ce80f92e866576c701ded72/maturin-1.10.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3ce9b2ad4fb9c341f450a6d32dc3edb409a2d582a81bc46ba55f6e3b6196b22", size = 8827021, upload-time = "2025-11-19T11:52:48.143Z" }, + { url = "https://files.pythonhosted.org/packages/7f/83/14c96ddc93b38745d8c3b85126f7d78a94f809a49dc9644bb22b0dc7b78c/maturin-1.10.2-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:f0d1b7b5f73c8d30a7e71cd2a2189a7f0126a3a3cd8b3d6843e7e1d4db50f759", size = 8751780, upload-time = "2025-11-19T11:52:51.613Z" }, + { url = "https://files.pythonhosted.org/packages/46/8d/753148c0d0472acd31a297f6d11c3263cd2668d38278ed29d523625f7290/maturin-1.10.2-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:efcd496a3202ffe0d0489df1f83d08b91399782fb2dd545d5a1e7bf6fd81af39", size = 9241884, upload-time = "2025-11-19T11:52:53.946Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f9/f5ca9fe8cad70cac6f3b6008598cc708f8a74dd619baced99784a6253f23/maturin-1.10.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a41ec70d99e27c05377be90f8e3c3def2a7bae4d0d9d5ea874aaf2d1da625d5c", size = 8671736, upload-time = "2025-11-19T11:52:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/0a/76/f59cbcfcabef0259c3971f8b5754c85276a272028d8363386b03ec4e9947/maturin-1.10.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:07a82864352feeaf2167247c8206937ef6c6ae9533025d416b7004ade0ea601d", size = 8633475, upload-time = "2025-11-19T11:53:00.389Z" }, + { url = "https://files.pythonhosted.org/packages/53/40/96cd959ad1dda6c12301860a74afece200a3209d84b393beedd5d7d915c0/maturin-1.10.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:04df81ee295dcda37828bd025a4ac688ea856e3946e4cb300a8f44a448de0069", size = 11177118, upload-time = "2025-11-19T11:53:03.014Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b6/144f180f36314be183f5237011528f0e39fe5fd2e74e65c3b44a5795971e/maturin-1.10.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96e1d391e4c1fa87edf2a37e4d53d5f2e5f39dd880b9d8306ac9f8eb212d23f8", size = 9320218, upload-time = "2025-11-19T11:53:05.39Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2d/2c483c1b3118e2e10fd8219d5291843f5f7c12284113251bf506144a3ac1/maturin-1.10.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a217aa7c42aa332fb8e8377eb07314e1f02cf0fe036f614aca4575121952addd", size = 8985266, upload-time = "2025-11-19T11:53:07.618Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/1d0222521e112cd058b56e8d96c72cf9615f799e3b557adb4b16004f42aa/maturin-1.10.2-py3-none-win32.whl", hash = "sha256:da031771d9fb6ddb1d373638ec2556feee29e4507365cd5749a2d354bcadd818", size = 7667897, upload-time = "2025-11-19T11:53:10.14Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ec/c6c973b1def0d04533620b439d5d7aebb257657ba66710885394514c8045/maturin-1.10.2-py3-none-win_amd64.whl", hash = "sha256:da777766fd584440dc9fecd30059a94f85e4983f58b09e438ae38ee4b494024c", size = 8908416, upload-time = "2025-11-19T11:53:12.862Z" }, + { url = "https://files.pythonhosted.org/packages/1b/01/7da60c9f7d5dc92dfa5e8888239fd0fb2613ee19e44e6db5c2ed5595fab3/maturin-1.10.2-py3-none-win_arm64.whl", hash = "sha256:a4c29a770ea2c76082e0afc6d4efd8ee94405588bfae00d10828f72e206c739b", size = 7506680, upload-time = "2025-11-19T11:53:15.403Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mplsoccer" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pandas", marker = "python_full_version < '3.10'" }, + { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "requests", marker = "python_full_version < '3.10'" }, + { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "seaborn", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/b9/b8ae3b1ea8d04ea5c3e2379490e72867f6fd474fbb27d57cce4c2e89afbb/mplsoccer-1.6.0.tar.gz", hash = "sha256:02c2a3c4056b0a4d79564f06639c6bc62550a87db22917cd424d5785fc34a15e", size = 832732, upload-time = "2025-10-26T19:15:52.282Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/ee/e77791bbe673852936a559d314a445bb57a880b2dceb3bc4769da69a6763/mplsoccer-1.6.0-py3-none-any.whl", hash = "sha256:34800fa3b26212dda7d366bbacd863bd2a5723563274288d99ec6737d14501d5", size = 88512, upload-time = "2025-10-26T19:15:50.525Z" }, +] + +[[package]] +name = "mplsoccer" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "matplotlib", version = "3.10.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas", marker = "python_full_version >= '3.10'" }, + { name = "pillow", version = "12.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "requests", marker = "python_full_version >= '3.10'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "seaborn", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/df/e810b16644a51d3055a0c0942d7f267fc08ae3d313545dcd4ad423ed61b9/mplsoccer-1.6.1.tar.gz", hash = "sha256:021d5369049d3f3f2aecade7b3a94240e0c2b5458835bdca09421a01ad9d0f15", size = 832792, upload-time = "2025-11-01T07:27:06.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/63/8dd3cdf8acac342d5011900c10d2a8238d648b796e0ab45c6603668d1741/mplsoccer-1.6.1-py3-none-any.whl", hash = "sha256:a8311a32290804b5d8d8aa4c117b2ec8d2444cbf72cb06ec40120072ee2da6dd", size = 88512, upload-time = "2025-11-01T07:27:08.247Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" }, + { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" }, + { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" }, + { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" }, + { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" }, + { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" }, + { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416, upload-time = "2026-01-26T02:43:22.703Z" }, + { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022, upload-time = "2026-01-26T02:43:23.77Z" }, + { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238, upload-time = "2026-01-26T02:43:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/74525ebe3eb5fddcd6735fc03cbea3feeed4122b53bc798ac32d297ac9ae/multidict-6.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f", size = 77107, upload-time = "2026-01-26T02:46:12.608Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9a/ce8744e777a74b3050b1bf56be3eed1053b3457302ea055f1ea437200a23/multidict-6.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358", size = 44943, upload-time = "2026-01-26T02:46:14.016Z" }, + { url = "https://files.pythonhosted.org/packages/83/9c/1d2a283d9c6f31e260cb6c2fccadc3edcf6c4c14ee0929cd2af4d2606dd7/multidict-6.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5", size = 44603, upload-time = "2026-01-26T02:46:15.391Z" }, + { url = "https://files.pythonhosted.org/packages/87/9d/3b186201671583d8e8d6d79c07481a5aafd0ba7575e3d8566baec80c1e82/multidict-6.7.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0", size = 240573, upload-time = "2026-01-26T02:46:16.783Z" }, + { url = "https://files.pythonhosted.org/packages/42/7d/a52f5d4d0754311d1ac78478e34dff88de71259a8585e05ee14e5f877caf/multidict-6.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8", size = 240106, upload-time = "2026-01-26T02:46:18.432Z" }, + { url = "https://files.pythonhosted.org/packages/84/9f/d80118e6c30ff55b7d171bdc5520aad4b9626e657520b8d7c8ca8c2fad12/multidict-6.7.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0", size = 219418, upload-time = "2026-01-26T02:46:20.526Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bd/896e60b3457f194de77c7de64f9acce9f75da0518a5230ce1df534f6747b/multidict-6.7.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f", size = 252124, upload-time = "2026-01-26T02:46:22.157Z" }, + { url = "https://files.pythonhosted.org/packages/f4/de/ba6b30447c36a37078d0ba604aa12c1a52887af0c355236ca6e0a9d5286f/multidict-6.7.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f", size = 249402, upload-time = "2026-01-26T02:46:23.718Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b2/50a383c96230e432895a2fd3bcfe1b65785899598259d871d5de6b93180c/multidict-6.7.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e", size = 240346, upload-time = "2026-01-26T02:46:25.393Z" }, + { url = "https://files.pythonhosted.org/packages/89/37/16d391fd8da544b1489306e38a46785fa41dd0f0ef766837ed7d4676dde0/multidict-6.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2", size = 237010, upload-time = "2026-01-26T02:46:27.408Z" }, + { url = "https://files.pythonhosted.org/packages/b0/24/3152ee026eda86d5d3e3685182911e6951af7a016579da931080ce6ac9ad/multidict-6.7.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8", size = 232018, upload-time = "2026-01-26T02:46:29.941Z" }, + { url = "https://files.pythonhosted.org/packages/9c/1f/48d3c27a72be7fd23a55d8847193c459959bf35a5bb5844530dab00b739b/multidict-6.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941", size = 241498, upload-time = "2026-01-26T02:46:32.052Z" }, + { url = "https://files.pythonhosted.org/packages/1a/45/413643ae2952d0decdf6c1250f86d08a43e143271441e81027e38d598bd7/multidict-6.7.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a", size = 247957, upload-time = "2026-01-26T02:46:33.666Z" }, + { url = "https://files.pythonhosted.org/packages/50/f8/f1d0ac23df15e0470776388bdb261506f63af1f81d28bacb5e262d6e12b6/multidict-6.7.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de", size = 241651, upload-time = "2026-01-26T02:46:35.7Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c9/1a2a18f383cf129add66b6c36b75c3911a7ba95cf26cb141482de085cc12/multidict-6.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5", size = 236371, upload-time = "2026-01-26T02:46:37.37Z" }, + { url = "https://files.pythonhosted.org/packages/bb/aa/77d87e3fca31325b87e0eb72d5fe9a7472dcb51391a42df7ac1f3842f6c0/multidict-6.7.1-cp39-cp39-win32.whl", hash = "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0", size = 41426, upload-time = "2026-01-26T02:46:39.026Z" }, + { url = "https://files.pythonhosted.org/packages/e3/b3/e8863e6a2da15a9d7e98976ff402e871b7352c76566df6c18d0378e0d9cf/multidict-6.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4", size = 46180, upload-time = "2026-01-26T02:46:40.422Z" }, + { url = "https://files.pythonhosted.org/packages/93/d3/dd4fa951ad5b5fa216bf30054d705683d13405eea7459833d78f31b74c9c/multidict-6.7.1-cp39-cp39-win_arm64.whl", hash = "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9", size = 43231, upload-time = "2026-01-26T02:46:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/b5/b58cdc25fadd424552804bf410855d52324183112aa004f0732c5f6324cf/mypy-1.19.0.tar.gz", hash = "sha256:f6b874ca77f733222641e5c46e4711648c4037ea13646fd0cdc814c2eaec2528", size = 3579025, upload-time = "2025-11-28T15:49:01.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/8f/55fb488c2b7dabd76e3f30c10f7ab0f6190c1fcbc3e97b1e588ec625bbe2/mypy-1.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6148ede033982a8c5ca1143de34c71836a09f105068aaa8b7d5edab2b053e6c8", size = 13093239, upload-time = "2025-11-28T15:45:11.342Z" }, + { url = "https://files.pythonhosted.org/packages/72/1b/278beea978456c56b3262266274f335c3ba5ff2c8108b3b31bec1ffa4c1d/mypy-1.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9ac09e52bb0f7fb912f5d2a783345c72441a08ef56ce3e17c1752af36340a39", size = 12156128, upload-time = "2025-11-28T15:46:02.566Z" }, + { url = "https://files.pythonhosted.org/packages/21/f8/e06f951902e136ff74fd7a4dc4ef9d884faeb2f8eb9c49461235714f079f/mypy-1.19.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f7254c15ab3f8ed68f8e8f5cbe88757848df793e31c36aaa4d4f9783fd08ab", size = 12753508, upload-time = "2025-11-28T15:44:47.538Z" }, + { url = "https://files.pythonhosted.org/packages/67/5a/d035c534ad86e09cee274d53cf0fd769c0b29ca6ed5b32e205be3c06878c/mypy-1.19.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318ba74f75899b0e78b847d8c50821e4c9637c79d9a59680fc1259f29338cb3e", size = 13507553, upload-time = "2025-11-28T15:44:39.26Z" }, + { url = "https://files.pythonhosted.org/packages/6a/17/c4a5498e00071ef29e483a01558b285d086825b61cf1fb2629fbdd019d94/mypy-1.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf7d84f497f78b682edd407f14a7b6e1a2212b433eedb054e2081380b7395aa3", size = 13792898, upload-time = "2025-11-28T15:44:31.102Z" }, + { url = "https://files.pythonhosted.org/packages/67/f6/bb542422b3ee4399ae1cdc463300d2d91515ab834c6233f2fd1d52fa21e0/mypy-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:c3385246593ac2b97f155a0e9639be906e73534630f663747c71908dfbf26134", size = 10048835, upload-time = "2025-11-28T15:48:15.744Z" }, + { url = "https://files.pythonhosted.org/packages/0f/d2/010fb171ae5ac4a01cc34fbacd7544531e5ace95c35ca166dd8fd1b901d0/mypy-1.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a31e4c28e8ddb042c84c5e977e28a21195d086aaffaf08b016b78e19c9ef8106", size = 13010563, upload-time = "2025-11-28T15:48:23.975Z" }, + { url = "https://files.pythonhosted.org/packages/41/6b/63f095c9f1ce584fdeb595d663d49e0980c735a1d2004720ccec252c5d47/mypy-1.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34ec1ac66d31644f194b7c163d7f8b8434f1b49719d403a5d26c87fff7e913f7", size = 12077037, upload-time = "2025-11-28T15:47:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/d7/83/6cb93d289038d809023ec20eb0b48bbb1d80af40511fa077da78af6ff7c7/mypy-1.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb64b0ba5980466a0f3f9990d1c582bcab8db12e29815ecb57f1408d99b4bff7", size = 12680255, upload-time = "2025-11-28T15:46:57.628Z" }, + { url = "https://files.pythonhosted.org/packages/99/db/d217815705987d2cbace2edd9100926196d6f85bcb9b5af05058d6e3c8ad/mypy-1.19.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:120cffe120cca5c23c03c77f84abc0c14c5d2e03736f6c312480020082f1994b", size = 13421472, upload-time = "2025-11-28T15:47:59.655Z" }, + { url = "https://files.pythonhosted.org/packages/4e/51/d2beaca7c497944b07594f3f8aad8d2f0e8fc53677059848ae5d6f4d193e/mypy-1.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7a500ab5c444268a70565e374fc803972bfd1f09545b13418a5174e29883dab7", size = 13651823, upload-time = "2025-11-28T15:45:29.318Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d1/7883dcf7644db3b69490f37b51029e0870aac4a7ad34d09ceae709a3df44/mypy-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:c14a98bc63fd867530e8ec82f217dae29d0550c86e70debc9667fff1ec83284e", size = 10049077, upload-time = "2025-11-28T15:45:39.818Z" }, + { url = "https://files.pythonhosted.org/packages/11/7e/1afa8fb188b876abeaa14460dc4983f909aaacaa4bf5718c00b2c7e0b3d5/mypy-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fb3115cb8fa7c5f887c8a8d81ccdcb94cff334684980d847e5a62e926910e1d", size = 13207728, upload-time = "2025-11-28T15:46:26.463Z" }, + { url = "https://files.pythonhosted.org/packages/b2/13/f103d04962bcbefb1644f5ccb235998b32c337d6c13145ea390b9da47f3e/mypy-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3e19e3b897562276bb331074d64c076dbdd3e79213f36eed4e592272dabd760", size = 12202945, upload-time = "2025-11-28T15:48:49.143Z" }, + { url = "https://files.pythonhosted.org/packages/e4/93/a86a5608f74a22284a8ccea8592f6e270b61f95b8588951110ad797c2ddd/mypy-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9d491295825182fba01b6ffe2c6fe4e5a49dbf4e2bb4d1217b6ced3b4797bc6", size = 12718673, upload-time = "2025-11-28T15:47:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/3d/58/cf08fff9ced0423b858f2a7495001fda28dc058136818ee9dffc31534ea9/mypy-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6016c52ab209919b46169651b362068f632efcd5eb8ef9d1735f6f86da7853b2", size = 13608336, upload-time = "2025-11-28T15:48:32.625Z" }, + { url = "https://files.pythonhosted.org/packages/64/ed/9c509105c5a6d4b73bb08733102a3ea62c25bc02c51bca85e3134bf912d3/mypy-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f188dcf16483b3e59f9278c4ed939ec0254aa8a60e8fc100648d9ab5ee95a431", size = 13833174, upload-time = "2025-11-28T15:45:48.091Z" }, + { url = "https://files.pythonhosted.org/packages/cd/71/01939b66e35c6f8cb3e6fdf0b657f0fd24de2f8ba5e523625c8e72328208/mypy-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:0e3c3d1e1d62e678c339e7ade72746a9e0325de42cd2cccc51616c7b2ed1a018", size = 10112208, upload-time = "2025-11-28T15:46:41.702Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0d/a1357e6bb49e37ce26fcf7e3cc55679ce9f4ebee0cd8b6ee3a0e301a9210/mypy-1.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7686ed65dbabd24d20066f3115018d2dce030d8fa9db01aa9f0a59b6813e9f9e", size = 13191993, upload-time = "2025-11-28T15:47:22.336Z" }, + { url = "https://files.pythonhosted.org/packages/5d/75/8e5d492a879ec4490e6ba664b5154e48c46c85b5ac9785792a5ec6a4d58f/mypy-1.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4a985b2e32f23bead72e2fb4bbe5d6aceee176be471243bd831d5b2644672d", size = 12174411, upload-time = "2025-11-28T15:44:55.492Z" }, + { url = "https://files.pythonhosted.org/packages/71/31/ad5dcee9bfe226e8eaba777e9d9d251c292650130f0450a280aec3485370/mypy-1.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc51a5b864f73a3a182584b1ac75c404396a17eced54341629d8bdcb644a5bba", size = 12727751, upload-time = "2025-11-28T15:44:14.169Z" }, + { url = "https://files.pythonhosted.org/packages/77/06/b6b8994ce07405f6039701f4b66e9d23f499d0b41c6dd46ec28f96d57ec3/mypy-1.19.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37af5166f9475872034b56c5efdcf65ee25394e9e1d172907b84577120714364", size = 13593323, upload-time = "2025-11-28T15:46:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/68/b1/126e274484cccdf099a8e328d4fda1c7bdb98a5e888fa6010b00e1bbf330/mypy-1.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:510c014b722308c9bd377993bcbf9a07d7e0692e5fa8fc70e639c1eb19fc6bee", size = 13818032, upload-time = "2025-11-28T15:46:18.286Z" }, + { url = "https://files.pythonhosted.org/packages/f8/56/53a8f70f562dfc466c766469133a8a4909f6c0012d83993143f2a9d48d2d/mypy-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:cabbee74f29aa9cd3b444ec2f1e4fa5a9d0d746ce7567a6a609e224429781f53", size = 10120644, upload-time = "2025-11-28T15:47:43.99Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f4/7751f32f56916f7f8c229fe902cbdba3e4dd3f3ea9e8b872be97e7fc546d/mypy-1.19.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f2e36bed3c6d9b5f35d28b63ca4b727cb0228e480826ffc8953d1892ddc8999d", size = 13185236, upload-time = "2025-11-28T15:45:20.696Z" }, + { url = "https://files.pythonhosted.org/packages/35/31/871a9531f09e78e8d145032355890384f8a5b38c95a2c7732d226b93242e/mypy-1.19.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a18d8abdda14035c5718acb748faec09571432811af129bf0d9e7b2d6699bf18", size = 12213902, upload-time = "2025-11-28T15:46:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/58/b8/af221910dd40eeefa2077a59107e611550167b9994693fc5926a0b0f87c0/mypy-1.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75e60aca3723a23511948539b0d7ed514dda194bc3755eae0bfc7a6b4887aa7", size = 12738600, upload-time = "2025-11-28T15:44:22.521Z" }, + { url = "https://files.pythonhosted.org/packages/11/9f/c39e89a3e319c1d9c734dedec1183b2cc3aefbab066ec611619002abb932/mypy-1.19.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f44f2ae3c58421ee05fe609160343c25f70e3967f6e32792b5a78006a9d850f", size = 13592639, upload-time = "2025-11-28T15:48:08.55Z" }, + { url = "https://files.pythonhosted.org/packages/97/6d/ffaf5f01f5e284d9033de1267e6c1b8f3783f2cf784465378a86122e884b/mypy-1.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63ea6a00e4bd6822adbfc75b02ab3653a17c02c4347f5bb0cf1d5b9df3a05835", size = 13799132, upload-time = "2025-11-28T15:47:06.032Z" }, + { url = "https://files.pythonhosted.org/packages/fe/b0/c33921e73aaa0106224e5a34822411bea38046188eb781637f5a5b07e269/mypy-1.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ad925b14a0bb99821ff6f734553294aa6a3440a8cb082fe1f5b84dfb662afb1", size = 10269832, upload-time = "2025-11-28T15:47:29.392Z" }, + { url = "https://files.pythonhosted.org/packages/b4/59/a7748ef43446163a93159d82bb270c6c4f3d94c1fcbdd2a29a7e439e74d7/mypy-1.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0dde5cb375cb94deff0d4b548b993bec52859d1651e073d63a1386d392a95495", size = 13094255, upload-time = "2025-11-28T15:47:14.282Z" }, + { url = "https://files.pythonhosted.org/packages/f5/0b/92ebf5abc83f559a35dcba3bd9227726b04b04178f1e521f38e647b930eb/mypy-1.19.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1cf9c59398db1c68a134b0b5354a09a1e124523f00bacd68e553b8bd16ff3299", size = 12161414, upload-time = "2025-11-28T15:45:03.302Z" }, + { url = "https://files.pythonhosted.org/packages/aa/03/19412f0a786722055a52c01b4c5d71e5b5443a89f6bbcdd445408240e217/mypy-1.19.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3210d87b30e6af9c8faed61be2642fcbe60ef77cec64fa1ef810a630a4cf671c", size = 12756782, upload-time = "2025-11-28T15:46:49.522Z" }, + { url = "https://files.pythonhosted.org/packages/cb/85/395d53c9098b251414b0448cdadcd3277523ff36f5abda6d26ff945dbdb3/mypy-1.19.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2c1101ab41d01303103ab6ef82cbbfedb81c1a060c868fa7cc013d573d37ab5", size = 13503492, upload-time = "2025-11-28T15:48:57.339Z" }, + { url = "https://files.pythonhosted.org/packages/dd/33/1ab1113e3778617ae7aba66b4b537f90512bd279ff65b6c984fb91fbb2d3/mypy-1.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ea4fd21bb48f0da49e6d3b37ef6bd7e8228b9fe41bbf4d80d9364d11adbd43c", size = 13787703, upload-time = "2025-11-28T15:48:41.286Z" }, + { url = "https://files.pythonhosted.org/packages/4f/2d/8b0821b3e0d538de1ad96c86502256c7326274d5cb74e0b373efaada273f/mypy-1.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:16f76ff3f3fd8137aadf593cb4607d82634fca675e8211ad75c43d86033ee6c6", size = 10049225, upload-time = "2025-11-28T15:45:55.089Z" }, + { url = "https://files.pythonhosted.org/packages/09/0e/fe228ed5aeab470c6f4eb82481837fadb642a5aa95cc8215fd2214822c10/mypy-1.19.0-py3-none-any.whl", hash = "sha256:0c01c99d626380752e527d5ce8e69ffbba2046eb8a060db0329690849cf9b6f9", size = 2469714, upload-time = "2025-11-28T15:45:33.22Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "numpy" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015, upload-time = "2024-08-26T20:19:40.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245, upload-time = "2024-08-26T20:04:14.625Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04", size = 13738540, upload-time = "2024-08-26T20:04:36.784Z" }, + { url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66", size = 5300623, upload-time = "2024-08-26T20:04:46.491Z" }, + { url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b", size = 6901774, upload-time = "2024-08-26T20:04:58.173Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd", size = 13907081, upload-time = "2024-08-26T20:05:19.098Z" }, + { url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318", size = 19523451, upload-time = "2024-08-26T20:05:47.479Z" }, + { url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8", size = 19927572, upload-time = "2024-08-26T20:06:17.137Z" }, + { url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326", size = 14400722, upload-time = "2024-08-26T20:06:39.16Z" }, + { url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170, upload-time = "2024-08-26T20:06:50.361Z" }, + { url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558, upload-time = "2024-08-26T20:07:13.881Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137, upload-time = "2024-08-26T20:07:45.345Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552, upload-time = "2024-08-26T20:08:06.666Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957, upload-time = "2024-08-26T20:08:15.83Z" }, + { url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573, upload-time = "2024-08-26T20:08:27.185Z" }, + { url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330, upload-time = "2024-08-26T20:08:48.058Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895, upload-time = "2024-08-26T20:09:16.536Z" }, + { url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253, upload-time = "2024-08-26T20:09:46.263Z" }, + { url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074, upload-time = "2024-08-26T20:10:08.483Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640, upload-time = "2024-08-26T20:10:19.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230, upload-time = "2024-08-26T20:10:43.413Z" }, + { url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803, upload-time = "2024-08-26T20:11:13.916Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835, upload-time = "2024-08-26T20:11:34.779Z" }, + { url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499, upload-time = "2024-08-26T20:11:43.902Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497, upload-time = "2024-08-26T20:11:55.09Z" }, + { url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158, upload-time = "2024-08-26T20:12:14.95Z" }, + { url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173, upload-time = "2024-08-26T20:12:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174, upload-time = "2024-08-26T20:13:13.634Z" }, + { url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701, upload-time = "2024-08-26T20:13:34.851Z" }, + { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313, upload-time = "2024-08-26T20:13:45.653Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179, upload-time = "2024-08-26T20:14:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c", size = 21169942, upload-time = "2024-08-26T20:14:40.108Z" }, + { url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd", size = 13711512, upload-time = "2024-08-26T20:15:00.985Z" }, + { url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b", size = 5306976, upload-time = "2024-08-26T20:15:10.876Z" }, + { url = "https://files.pythonhosted.org/packages/2d/98/121996dcfb10a6087a05e54453e28e58694a7db62c5a5a29cee14c6e047b/numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729", size = 6906494, upload-time = "2024-08-26T20:15:22.055Z" }, + { url = "https://files.pythonhosted.org/packages/15/31/9dffc70da6b9bbf7968f6551967fc21156207366272c2a40b4ed6008dc9b/numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1", size = 13912596, upload-time = "2024-08-26T20:15:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/b9/14/78635daab4b07c0930c919d451b8bf8c164774e6a3413aed04a6d95758ce/numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd", size = 19526099, upload-time = "2024-08-26T20:16:11.048Z" }, + { url = "https://files.pythonhosted.org/packages/26/4c/0eeca4614003077f68bfe7aac8b7496f04221865b3a5e7cb230c9d055afd/numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d", size = 19932823, upload-time = "2024-08-26T20:16:40.171Z" }, + { url = "https://files.pythonhosted.org/packages/f1/46/ea25b98b13dccaebddf1a803f8c748680d972e00507cd9bc6dcdb5aa2ac1/numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d", size = 14404424, upload-time = "2024-08-26T20:17:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809, upload-time = "2024-08-26T20:17:13.553Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314, upload-time = "2024-08-26T20:17:36.72Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3b/df5a870ac6a3be3a86856ce195ef42eec7ae50d2a202be1f5a4b3b340e14/numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8", size = 21025288, upload-time = "2024-08-26T20:18:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/2c/97/51af92f18d6f6f2d9ad8b482a99fb74e142d71372da5d834b3a2747a446e/numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4", size = 6762793, upload-time = "2024-08-26T20:18:19.125Z" }, + { url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c", size = 19334885, upload-time = "2024-08-26T20:18:47.237Z" }, + { url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784, upload-time = "2024-08-26T20:19:11.19Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324, upload-time = "2025-11-16T22:49:22.582Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872, upload-time = "2025-11-16T22:49:25.408Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148, upload-time = "2025-11-16T22:49:27.549Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282, upload-time = "2025-11-16T22:49:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903, upload-time = "2025-11-16T22:49:34.191Z" }, + { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672, upload-time = "2025-11-16T22:49:37.2Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896, upload-time = "2025-11-16T22:49:39.727Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608, upload-time = "2025-11-16T22:49:42.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442, upload-time = "2025-11-16T22:49:43.99Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555, upload-time = "2025-11-16T22:49:47.092Z" }, + { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, + { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, + { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, + { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" }, + { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" }, + { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" }, + { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" }, + { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" }, + { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" }, + { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" }, + { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" }, + { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" }, + { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" }, + { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" }, + { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, + { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, + { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" }, + { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" }, + { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" }, + { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" }, + { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" }, + { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" }, + { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, + { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689, upload-time = "2025-11-16T22:52:23.247Z" }, + { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053, upload-time = "2025-11-16T22:52:26.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635, upload-time = "2025-11-16T22:52:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770, upload-time = "2025-11-16T22:52:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768, upload-time = "2025-11-16T22:52:33.593Z" }, + { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263, upload-time = "2025-11-16T22:52:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, + { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, + { url = "https://files.pythonhosted.org/packages/56/b4/52eeb530a99e2a4c55ffcd352772b599ed4473a0f892d127f4147cf0f88e/pandas-2.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2", size = 11567720, upload-time = "2025-09-29T23:33:06.209Z" }, + { url = "https://files.pythonhosted.org/packages/48/4a/2d8b67632a021bced649ba940455ed441ca854e57d6e7658a6024587b083/pandas-2.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8", size = 10810302, upload-time = "2025-09-29T23:33:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/13/e6/d2465010ee0569a245c975dc6967b801887068bc893e908239b1f4b6c1ac/pandas-2.3.3-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff", size = 12154874, upload-time = "2025-09-29T23:33:49.939Z" }, + { url = "https://files.pythonhosted.org/packages/1f/18/aae8c0aa69a386a3255940e9317f793808ea79d0a525a97a903366bb2569/pandas-2.3.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29", size = 12790141, upload-time = "2025-09-29T23:34:05.655Z" }, + { url = "https://files.pythonhosted.org/packages/f7/26/617f98de789de00c2a444fbe6301bb19e66556ac78cff933d2c98f62f2b4/pandas-2.3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73", size = 13208697, upload-time = "2025-09-29T23:34:21.835Z" }, + { url = "https://files.pythonhosted.org/packages/b9/fb/25709afa4552042bd0e15717c75e9b4a2294c3dc4f7e6ea50f03c5136600/pandas-2.3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9", size = 13879233, upload-time = "2025-09-29T23:34:35.079Z" }, + { url = "https://files.pythonhosted.org/packages/98/af/7be05277859a7bc399da8ba68b88c96b27b48740b6cf49688899c6eb4176/pandas-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa", size = 11359119, upload-time = "2025-09-29T23:34:46.339Z" }, +] + +[[package]] +name = "pandas-stubs" +version = "2.2.2.240807" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "types-pytz", version = "2025.2.0.20251108", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/df/0da95bc75c76f1e012e0bc0b76da31faaf4254e94b9870f25e6311145e98/pandas_stubs-2.2.2.240807.tar.gz", hash = "sha256:64a559725a57a449f46225fbafc422520b7410bff9252b661a225b5559192a93", size = 103095, upload-time = "2024-08-07T12:30:54.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/f9/22c91632ea1b4c6165952f677bf9ad95f9ac36ffd7ef3e6450144e6d8b1a/pandas_stubs-2.2.2.240807-py3-none-any.whl", hash = "sha256:893919ad82be4275f0d07bb47a95d08bae580d3fdea308a7acfcb3f02e76186e", size = 157069, upload-time = "2024-08-07T12:30:51.868Z" }, +] + +[[package]] +name = "pandas-stubs" +version = "2.3.3.260113" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "types-pytz", version = "2026.1.1.20260304", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/5d/be23854a73fda69f1dbdda7bc10fbd6f930bd1fa87aaec389f00c901c1e8/pandas_stubs-2.3.3.260113.tar.gz", hash = "sha256:076e3724bcaa73de78932b012ec64b3010463d377fa63116f4e6850643d93800", size = 116131, upload-time = "2026-01-13T22:30:16.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/c6/df1fe324248424f77b89371116dab5243db7f052c32cc9fe7442ad9c5f75/pandas_stubs-2.3.3.260113-py3-none-any.whl", hash = "sha256:ec070b5c576e1badf12544ae50385872f0631fc35d99d00dc598c2954ec564d3", size = 168246, upload-time = "2026-01-13T22:30:15.244Z" }, +] + +[[package]] +name = "pandas-stubs" +version = "3.0.0.260204" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/1d/297ff2c7ea50a768a2247621d6451abb2a07c0e9be7ca6d36ebe371658e5/pandas_stubs-3.0.0.260204.tar.gz", hash = "sha256:bf9294b76352effcffa9cb85edf0bed1339a7ec0c30b8e1ac3d66b4228f1fbc3", size = 109383, upload-time = "2026-02-04T15:17:17.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/2f/f91e4eee21585ff548e83358332d5632ee49f6b2dcd96cb5dca4e0468951/pandas_stubs-3.0.0.260204-py3-none-any.whl", hash = "sha256:5ab9e4d55a6e2752e9720828564af40d48c4f709e6a2c69b743014a6fcb6c241", size = 168540, upload-time = "2026-02-04T15:17:15.615Z" }, +] + +[[package]] +name = "parso" +version = "0.8.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pdoc" +version = "6.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "pygments" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/bc/e14e4f717730fd50b596f659bcc5cada0868537787fa28cd83ad84f67e67/pdoc-6.4.4-py3-none-any.whl", hash = "sha256:a1ca95cfd1c25b10947e50a10ffa261e3269f67c5f520b243d1c8e8302191f56", size = 92519, upload-time = "2021-04-21T11:25:16.001Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "pillow" +version = "11.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554, upload-time = "2025-07-01T09:13:39.342Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548, upload-time = "2025-07-01T09:13:41.835Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742, upload-time = "2025-07-03T13:09:47.439Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087, upload-time = "2025-07-03T13:09:51.796Z" }, + { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350, upload-time = "2025-07-01T09:13:43.865Z" }, + { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840, upload-time = "2025-07-01T09:13:46.161Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005, upload-time = "2025-07-01T09:13:47.829Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372, upload-time = "2025-07-01T09:13:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090, upload-time = "2025-07-01T09:13:53.915Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988, upload-time = "2025-07-01T09:13:55.699Z" }, + { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899, upload-time = "2025-07-01T09:13:57.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, + { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, + { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, + { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, + { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, + { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, + { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, + { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, + { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, + { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, + { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, + { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, + { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, + { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, + { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, + { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, + { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, + { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, + { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8e/9c089f01677d1264ab8648352dcb7773f37da6ad002542760c80107da816/pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f", size = 5316478, upload-time = "2025-07-01T09:15:52.209Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a9/5749930caf674695867eb56a581e78eb5f524b7583ff10b01b6e5048acb3/pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081", size = 4686522, upload-time = "2025-07-01T09:15:54.162Z" }, + { url = "https://files.pythonhosted.org/packages/43/46/0b85b763eb292b691030795f9f6bb6fcaf8948c39413c81696a01c3577f7/pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4", size = 5853376, upload-time = "2025-07-03T13:11:01.066Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/1a230ec0067243cbd60bc2dad5dc3ab46a8a41e21c15f5c9b52b26873069/pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc", size = 7626020, upload-time = "2025-07-03T13:11:06.479Z" }, + { url = "https://files.pythonhosted.org/packages/63/dd/f296c27ffba447bfad76c6a0c44c1ea97a90cb9472b9304c94a732e8dbfb/pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06", size = 5956732, upload-time = "2025-07-01T09:15:56.111Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a0/98a3630f0b57f77bae67716562513d3032ae70414fcaf02750279c389a9e/pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a", size = 6624404, upload-time = "2025-07-01T09:15:58.245Z" }, + { url = "https://files.pythonhosted.org/packages/de/e6/83dfba5646a290edd9a21964da07674409e410579c341fc5b8f7abd81620/pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978", size = 6067760, upload-time = "2025-07-01T09:16:00.003Z" }, + { url = "https://files.pythonhosted.org/packages/bc/41/15ab268fe6ee9a2bc7391e2bbb20a98d3974304ab1a406a992dcb297a370/pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d", size = 6700534, upload-time = "2025-07-01T09:16:02.29Z" }, + { url = "https://files.pythonhosted.org/packages/64/79/6d4f638b288300bed727ff29f2a3cb63db054b33518a95f27724915e3fbc/pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71", size = 6277091, upload-time = "2025-07-01T09:16:04.4Z" }, + { url = "https://files.pythonhosted.org/packages/46/05/4106422f45a05716fd34ed21763f8ec182e8ea00af6e9cb05b93a247361a/pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada", size = 6986091, upload-time = "2025-07-01T09:16:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/287fd55c2c12761d0591549d48885187579b7c257bef0c6660755b0b59ae/pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb", size = 2422632, upload-time = "2025-07-01T09:16:08.142Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556, upload-time = "2025-07-01T09:16:09.961Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625, upload-time = "2025-07-01T09:16:11.913Z" }, + { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207, upload-time = "2025-07-03T13:11:10.201Z" }, + { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939, upload-time = "2025-07-03T13:11:15.68Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166, upload-time = "2025-07-01T09:16:13.74Z" }, + { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482, upload-time = "2025-07-01T09:16:16.107Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596, upload-time = "2025-07-01T09:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, + { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, + { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, +] + +[[package]] +name = "pillow" +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/30/5bd3d794762481f8c8ae9c80e7b76ecea73b916959eb587521358ef0b2f9/pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0", size = 5304099, upload-time = "2026-02-11T04:20:06.13Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c1/aab9e8f3eeb4490180e357955e15c2ef74b31f64790ff356c06fb6cf6d84/pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713", size = 4657880, upload-time = "2026-02-11T04:20:09.291Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0a/9879e30d56815ad529d3985aeff5af4964202425c27261a6ada10f7cbf53/pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b", size = 6222587, upload-time = "2026-02-11T04:20:10.82Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5f/a1b72ff7139e4f89014e8d451442c74a774d5c43cd938fb0a9f878576b37/pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b", size = 8027678, upload-time = "2026-02-11T04:20:12.455Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c2/c7cb187dac79a3d22c3ebeae727abee01e077c8c7d930791dc592f335153/pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4", size = 6335777, upload-time = "2026-02-11T04:20:14.441Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7b/f9b09a7804ec7336effb96c26d37c29d27225783dc1501b7d62dcef6ae25/pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4", size = 7027140, upload-time = "2026-02-11T04:20:16.387Z" }, + { url = "https://files.pythonhosted.org/packages/98/b2/2fa3c391550bd421b10849d1a2144c44abcd966daadd2f7c12e19ea988c4/pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e", size = 6449855, upload-time = "2026-02-11T04:20:18.554Z" }, + { url = "https://files.pythonhosted.org/packages/96/ff/9caf4b5b950c669263c39e96c78c0d74a342c71c4f43fd031bb5cb7ceac9/pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff", size = 7151329, upload-time = "2026-02-11T04:20:20.646Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f8/4b24841f582704da675ca535935bccb32b00a6da1226820845fac4a71136/pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40", size = 6325574, upload-time = "2026-02-11T04:20:22.43Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/9f6b01c0881d7036063aa6612ef04c0e2cad96be21325a1e92d0203f8e91/pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23", size = 7032347, upload-time = "2026-02-11T04:20:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/79/13/c7922edded3dcdaf10c59297540b72785620abc0538872c819915746757d/pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9", size = 2453457, upload-time = "2026-02-11T04:20:25.392Z" }, + { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" }, + { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" }, + { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" }, + { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" }, + { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" }, + { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, + { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, + { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, + { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, + { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, + { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, + { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, + { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, + { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" }, + { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" }, + { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" }, + { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "polars" +version = "1.36.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "polars-runtime-32" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/dc/56f2a90c79a2cb13f9e956eab6385effe54216ae7a2068b3a6406bae4345/polars-1.36.1.tar.gz", hash = "sha256:12c7616a2305559144711ab73eaa18814f7aa898c522e7645014b68f1432d54c", size = 711993, upload-time = "2025-12-10T01:14:53.033Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/c6/36a1b874036b49893ecae0ac44a2f63d1a76e6212631a5b2f50a86e0e8af/polars-1.36.1-py3-none-any.whl", hash = "sha256:853c1bbb237add6a5f6d133c15094a9b727d66dd6a4eb91dbb07cdb056b2b8ef", size = 802429, upload-time = "2025-12-10T01:13:53.838Z" }, +] + +[[package]] +name = "polars-runtime-32" +version = "1.36.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/df/597c0ef5eb8d761a16d72327846599b57c5d40d7f9e74306fc154aba8c37/polars_runtime_32-1.36.1.tar.gz", hash = "sha256:201c2cfd80ceb5d5cd7b63085b5fd08d6ae6554f922bcb941035e39638528a09", size = 2788751, upload-time = "2025-12-10T01:14:54.172Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/ea/871129a2d296966c0925b078a9a93c6c5e7facb1c5eebfcd3d5811aeddc1/polars_runtime_32-1.36.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:327b621ca82594f277751f7e23d4b939ebd1be18d54b4cdf7a2f8406cecc18b2", size = 43494311, upload-time = "2025-12-10T01:13:56.096Z" }, + { url = "https://files.pythonhosted.org/packages/d8/76/0038210ad1e526ce5bb2933b13760d6b986b3045eccc1338e661bd656f77/polars_runtime_32-1.36.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ab0d1f23084afee2b97de8c37aa3e02ec3569749ae39571bd89e7a8b11ae9e83", size = 39300602, upload-time = "2025-12-10T01:13:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/54/1e/2707bee75a780a953a77a2c59829ee90ef55708f02fc4add761c579bf76e/polars_runtime_32-1.36.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:899b9ad2e47ceb31eb157f27a09dbc2047efbf4969a923a6b1ba7f0412c3e64c", size = 44511780, upload-time = "2025-12-10T01:14:02.285Z" }, + { url = "https://files.pythonhosted.org/packages/11/b2/3fede95feee441be64b4bcb32444679a8fbb7a453a10251583053f6efe52/polars_runtime_32-1.36.1-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:d9d077bb9df711bc635a86540df48242bb91975b353e53ef261c6fae6cb0948f", size = 40688448, upload-time = "2025-12-10T01:14:05.131Z" }, + { url = "https://files.pythonhosted.org/packages/05/0f/e629713a72999939b7b4bfdbf030a32794db588b04fdf3dc977dd8ea6c53/polars_runtime_32-1.36.1-cp39-abi3-win_amd64.whl", hash = "sha256:cc17101f28c9a169ff8b5b8d4977a3683cd403621841623825525f440b564cf0", size = 44464898, upload-time = "2025-12-10T01:14:08.296Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d8/a12e6aa14f63784cead437083319ec7cece0d5bb9a5bfe7678cc6578b52a/polars_runtime_32-1.36.1-cp39-abi3-win_arm64.whl", hash = "sha256:809e73857be71250141225ddd5d2b30c97e6340aeaa0d445f930e01bef6888dc", size = 39798896, upload-time = "2025-12-10T01:14:11.568Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "cfgv", version = "3.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "identify", version = "2.6.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "nodeenv", marker = "python_full_version < '3.10'" }, + { name = "pyyaml", marker = "python_full_version < '3.10'" }, + { name = "virtualenv", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "cfgv", version = "3.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "identify", version = "2.6.17", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "nodeenv", marker = "python_full_version >= '3.10'" }, + { name = "pyyaml", marker = "python_full_version >= '3.10'" }, + { name = "virtualenv", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, + { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/9b/01/0ebaec9003f5d619a7475165961f8e3083cf8644d704b60395df3601632d/propcache-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff", size = 80277, upload-time = "2025-10-08T19:48:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/34/58/04af97ac586b4ef6b9026c3fd36ee7798b737a832f5d3440a4280dcebd3a/propcache-0.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb", size = 45865, upload-time = "2025-10-08T19:48:37.859Z" }, + { url = "https://files.pythonhosted.org/packages/7c/19/b65d98ae21384518b291d9939e24a8aeac4fdb5101b732576f8f7540e834/propcache-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac", size = 47636, upload-time = "2025-10-08T19:48:39.038Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/317048c6d91c356c7154dca5af019e6effeb7ee15fa6a6db327cc19e12b4/propcache-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888", size = 201126, upload-time = "2025-10-08T19:48:40.774Z" }, + { url = "https://files.pythonhosted.org/packages/71/69/0b2a7a5a6ee83292b4b997dbd80549d8ce7d40b6397c1646c0d9495f5a85/propcache-0.4.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc", size = 209837, upload-time = "2025-10-08T19:48:42.167Z" }, + { url = "https://files.pythonhosted.org/packages/a5/92/c699ac495a6698df6e497fc2de27af4b6ace10d8e76528357ce153722e45/propcache-0.4.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a", size = 215578, upload-time = "2025-10-08T19:48:43.56Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ee/14de81c5eb02c0ee4f500b4e39c4e1bd0677c06e72379e6ab18923c773fc/propcache-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88", size = 197187, upload-time = "2025-10-08T19:48:45.309Z" }, + { url = "https://files.pythonhosted.org/packages/1d/94/48dce9aaa6d8dd5a0859bad75158ec522546d4ac23f8e2f05fac469477dd/propcache-0.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00", size = 193478, upload-time = "2025-10-08T19:48:47.743Z" }, + { url = "https://files.pythonhosted.org/packages/60/b5/0516b563e801e1ace212afde869a0596a0d7115eec0b12d296d75633fb29/propcache-0.4.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0", size = 190650, upload-time = "2025-10-08T19:48:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/24/89/e0f7d4a5978cd56f8cd67735f74052f257dc471ec901694e430f0d1572fe/propcache-0.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e", size = 200251, upload-time = "2025-10-08T19:48:51.4Z" }, + { url = "https://files.pythonhosted.org/packages/06/7d/a1fac863d473876ed4406c914f2e14aa82d2f10dd207c9e16fc383cc5a24/propcache-0.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781", size = 200919, upload-time = "2025-10-08T19:48:53.227Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4e/f86a256ff24944cf5743e4e6c6994e3526f6acfcfb55e21694c2424f758c/propcache-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183", size = 193211, upload-time = "2025-10-08T19:48:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/6e/3f/3fbad5f4356b068f1b047d300a6ff2c66614d7030f078cd50be3fec04228/propcache-0.4.1-cp39-cp39-win32.whl", hash = "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19", size = 38314, upload-time = "2025-10-08T19:48:56.792Z" }, + { url = "https://files.pythonhosted.org/packages/a4/45/d78d136c3a3d215677abb886785aae744da2c3005bcb99e58640c56529b1/propcache-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f", size = 41912, upload-time = "2025-10-08T19:48:57.995Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2a/b0632941f25139f4e58450b307242951f7c2717a5704977c6d5323a800af/propcache-0.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938", size = 38450, upload-time = "2025-10-08T19:48:59.349Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, +] + +[[package]] +name = "pyarrow" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487, upload-time = "2025-07-18T00:57:31.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/d9/110de31880016e2afc52d8580b397dbe47615defbf09ca8cf55f56c62165/pyarrow-21.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e563271e2c5ff4d4a4cbeb2c83d5cf0d4938b891518e676025f7268c6fe5fe26", size = 31196837, upload-time = "2025-07-18T00:54:34.755Z" }, + { url = "https://files.pythonhosted.org/packages/df/5f/c1c1997613abf24fceb087e79432d24c19bc6f7259cab57c2c8e5e545fab/pyarrow-21.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:fee33b0ca46f4c85443d6c450357101e47d53e6c3f008d658c27a2d020d44c79", size = 32659470, upload-time = "2025-07-18T00:54:38.329Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ed/b1589a777816ee33ba123ba1e4f8f02243a844fed0deec97bde9fb21a5cf/pyarrow-21.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7be45519b830f7c24b21d630a31d48bcebfd5d4d7f9d3bdb49da9cdf6d764edb", size = 41055619, upload-time = "2025-07-18T00:54:42.172Z" }, + { url = "https://files.pythonhosted.org/packages/44/28/b6672962639e85dc0ac36f71ab3a8f5f38e01b51343d7aa372a6b56fa3f3/pyarrow-21.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:26bfd95f6bff443ceae63c65dc7e048670b7e98bc892210acba7e4995d3d4b51", size = 42733488, upload-time = "2025-07-18T00:54:47.132Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cc/de02c3614874b9089c94eac093f90ca5dfa6d5afe45de3ba847fd950fdf1/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd04ec08f7f8bd113c55868bd3fc442a9db67c27af098c5f814a3091e71cc61a", size = 43329159, upload-time = "2025-07-18T00:54:51.686Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3e/99473332ac40278f196e105ce30b79ab8affab12f6194802f2593d6b0be2/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9b0b14b49ac10654332a805aedfc0147fb3469cbf8ea951b3d040dab12372594", size = 45050567, upload-time = "2025-07-18T00:54:56.679Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f5/c372ef60593d713e8bfbb7e0c743501605f0ad00719146dc075faf11172b/pyarrow-21.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:9d9f8bcb4c3be7738add259738abdeddc363de1b80e3310e04067aa1ca596634", size = 26217959, upload-time = "2025-07-18T00:55:00.482Z" }, + { url = "https://files.pythonhosted.org/packages/94/dc/80564a3071a57c20b7c32575e4a0120e8a330ef487c319b122942d665960/pyarrow-21.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c077f48aab61738c237802836fc3844f85409a46015635198761b0d6a688f87b", size = 31243234, upload-time = "2025-07-18T00:55:03.812Z" }, + { url = "https://files.pythonhosted.org/packages/ea/cc/3b51cb2db26fe535d14f74cab4c79b191ed9a8cd4cbba45e2379b5ca2746/pyarrow-21.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:689f448066781856237eca8d1975b98cace19b8dd2ab6145bf49475478bcaa10", size = 32714370, upload-time = "2025-07-18T00:55:07.495Z" }, + { url = "https://files.pythonhosted.org/packages/24/11/a4431f36d5ad7d83b87146f515c063e4d07ef0b7240876ddb885e6b44f2e/pyarrow-21.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:479ee41399fcddc46159a551705b89c05f11e8b8cb8e968f7fec64f62d91985e", size = 41135424, upload-time = "2025-07-18T00:55:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/74/dc/035d54638fc5d2971cbf1e987ccd45f1091c83bcf747281cf6cc25e72c88/pyarrow-21.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:40ebfcb54a4f11bcde86bc586cbd0272bac0d516cfa539c799c2453768477569", size = 42823810, upload-time = "2025-07-18T00:55:16.301Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3b/89fced102448a9e3e0d4dded1f37fa3ce4700f02cdb8665457fcc8015f5b/pyarrow-21.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8d58d8497814274d3d20214fbb24abcad2f7e351474357d552a8d53bce70c70e", size = 43391538, upload-time = "2025-07-18T00:55:23.82Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/ea7f1bd08978d39debd3b23611c293f64a642557e8141c80635d501e6d53/pyarrow-21.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:585e7224f21124dd57836b1530ac8f2df2afc43c861d7bf3d58a4870c42ae36c", size = 45120056, upload-time = "2025-07-18T00:55:28.231Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0b/77ea0600009842b30ceebc3337639a7380cd946061b620ac1a2f3cb541e2/pyarrow-21.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:555ca6935b2cbca2c0e932bedd853e9bc523098c39636de9ad4693b5b1df86d6", size = 26220568, upload-time = "2025-07-18T00:55:32.122Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d4/d4f817b21aacc30195cf6a46ba041dd1be827efa4a623cc8bf39a1c2a0c0/pyarrow-21.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:3a302f0e0963db37e0a24a70c56cf91a4faa0bca51c23812279ca2e23481fccd", size = 31160305, upload-time = "2025-07-18T00:55:35.373Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9c/dcd38ce6e4b4d9a19e1d36914cb8e2b1da4e6003dd075474c4cfcdfe0601/pyarrow-21.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:b6b27cf01e243871390474a211a7922bfbe3bda21e39bc9160daf0da3fe48876", size = 32684264, upload-time = "2025-07-18T00:55:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/4f/74/2a2d9f8d7a59b639523454bec12dba35ae3d0a07d8ab529dc0809f74b23c/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e72a8ec6b868e258a2cd2672d91f2860ad532d590ce94cdf7d5e7ec674ccf03d", size = 41108099, upload-time = "2025-07-18T00:55:42.889Z" }, + { url = "https://files.pythonhosted.org/packages/ad/90/2660332eeb31303c13b653ea566a9918484b6e4d6b9d2d46879a33ab0622/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b7ae0bbdc8c6674259b25bef5d2a1d6af5d39d7200c819cf99e07f7dfef1c51e", size = 42829529, upload-time = "2025-07-18T00:55:47.069Z" }, + { url = "https://files.pythonhosted.org/packages/33/27/1a93a25c92717f6aa0fca06eb4700860577d016cd3ae51aad0e0488ac899/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:58c30a1729f82d201627c173d91bd431db88ea74dcaa3885855bc6203e433b82", size = 43367883, upload-time = "2025-07-18T00:55:53.069Z" }, + { url = "https://files.pythonhosted.org/packages/05/d9/4d09d919f35d599bc05c6950095e358c3e15148ead26292dfca1fb659b0c/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:072116f65604b822a7f22945a7a6e581cfa28e3454fdcc6939d4ff6090126623", size = 45133802, upload-time = "2025-07-18T00:55:57.714Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/f3795b6e192c3ab881325ffe172e526499eb3780e306a15103a2764916a2/pyarrow-21.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf56ec8b0a5c8c9d7021d6fd754e688104f9ebebf1bf4449613c9531f5346a18", size = 26203175, upload-time = "2025-07-18T00:56:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/16/ca/c7eaa8e62db8fb37ce942b1ea0c6d7abfe3786ca193957afa25e71b81b66/pyarrow-21.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e99310a4ebd4479bcd1964dff9e14af33746300cb014aa4a3781738ac63baf4a", size = 31154306, upload-time = "2025-07-18T00:56:04.42Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e8/e87d9e3b2489302b3a1aea709aaca4b781c5252fcb812a17ab6275a9a484/pyarrow-21.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:d2fe8e7f3ce329a71b7ddd7498b3cfac0eeb200c2789bd840234f0dc271a8efe", size = 32680622, upload-time = "2025-07-18T00:56:07.505Z" }, + { url = "https://files.pythonhosted.org/packages/84/52/79095d73a742aa0aba370c7942b1b655f598069489ab387fe47261a849e1/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f522e5709379d72fb3da7785aa489ff0bb87448a9dc5a75f45763a795a089ebd", size = 41104094, upload-time = "2025-07-18T00:56:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/89/4b/7782438b551dbb0468892a276b8c789b8bbdb25ea5c5eb27faadd753e037/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:69cbbdf0631396e9925e048cfa5bce4e8c3d3b41562bbd70c685a8eb53a91e61", size = 42825576, upload-time = "2025-07-18T00:56:15.569Z" }, + { url = "https://files.pythonhosted.org/packages/b3/62/0f29de6e0a1e33518dec92c65be0351d32d7ca351e51ec5f4f837a9aab91/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:731c7022587006b755d0bdb27626a1a3bb004bb56b11fb30d98b6c1b4718579d", size = 43368342, upload-time = "2025-07-18T00:56:19.531Z" }, + { url = "https://files.pythonhosted.org/packages/90/c7/0fa1f3f29cf75f339768cc698c8ad4ddd2481c1742e9741459911c9ac477/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc56bc708f2d8ac71bd1dcb927e458c93cec10b98eb4120206a4091db7b67b99", size = 45131218, upload-time = "2025-07-18T00:56:23.347Z" }, + { url = "https://files.pythonhosted.org/packages/01/63/581f2076465e67b23bc5a37d4a2abff8362d389d29d8105832e82c9c811c/pyarrow-21.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:186aa00bca62139f75b7de8420f745f2af12941595bbbfa7ed3870ff63e25636", size = 26087551, upload-time = "2025-07-18T00:56:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ab/357d0d9648bb8241ee7348e564f2479d206ebe6e1c47ac5027c2e31ecd39/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:a7a102574faa3f421141a64c10216e078df467ab9576684d5cd696952546e2da", size = 31290064, upload-time = "2025-07-18T00:56:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8a/5685d62a990e4cac2043fc76b4661bf38d06efed55cf45a334b455bd2759/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:1e005378c4a2c6db3ada3ad4c217b381f6c886f0a80d6a316fe586b90f77efd7", size = 32727837, upload-time = "2025-07-18T00:56:33.935Z" }, + { url = "https://files.pythonhosted.org/packages/fc/de/c0828ee09525c2bafefd3e736a248ebe764d07d0fd762d4f0929dbc516c9/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:65f8e85f79031449ec8706b74504a316805217b35b6099155dd7e227eef0d4b6", size = 41014158, upload-time = "2025-07-18T00:56:37.528Z" }, + { url = "https://files.pythonhosted.org/packages/6e/26/a2865c420c50b7a3748320b614f3484bfcde8347b2639b2b903b21ce6a72/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3a81486adc665c7eb1a2bde0224cfca6ceaba344a82a971ef059678417880eb8", size = 42667885, upload-time = "2025-07-18T00:56:41.483Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f9/4ee798dc902533159250fb4321267730bc0a107d8c6889e07c3add4fe3a5/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fc0d2f88b81dcf3ccf9a6ae17f89183762c8a94a5bdcfa09e05cfe413acf0503", size = 43276625, upload-time = "2025-07-18T00:56:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/5a/da/e02544d6997037a4b0d22d8e5f66bc9315c3671371a8b18c79ade1cefe14/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6299449adf89df38537837487a4f8d3bd91ec94354fdd2a7d30bc11c48ef6e79", size = 44951890, upload-time = "2025-07-18T00:56:52.568Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4e/519c1bc1876625fe6b71e9a28287c43ec2f20f73c658b9ae1d485c0c206e/pyarrow-21.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:222c39e2c70113543982c6b34f3077962b44fca38c0bd9e68bb6781534425c10", size = 26371006, upload-time = "2025-07-18T00:56:56.379Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cc/ce4939f4b316457a083dc5718b3982801e8c33f921b3c98e7a93b7c7491f/pyarrow-21.0.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:a7f6524e3747e35f80744537c78e7302cd41deee8baa668d56d55f77d9c464b3", size = 31211248, upload-time = "2025-07-18T00:56:59.7Z" }, + { url = "https://files.pythonhosted.org/packages/1f/c2/7a860931420d73985e2f340f06516b21740c15b28d24a0e99a900bb27d2b/pyarrow-21.0.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:203003786c9fd253ebcafa44b03c06983c9c8d06c3145e37f1b76a1f317aeae1", size = 32676896, upload-time = "2025-07-18T00:57:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/197f989b9a75e59b4ca0db6a13c56f19a0ad8a298c68da9cc28145e0bb97/pyarrow-21.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3b4d97e297741796fead24867a8dabf86c87e4584ccc03167e4a811f50fdf74d", size = 41067862, upload-time = "2025-07-18T00:57:07.587Z" }, + { url = "https://files.pythonhosted.org/packages/fa/82/6ecfa89487b35aa21accb014b64e0a6b814cc860d5e3170287bf5135c7d8/pyarrow-21.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:898afce396b80fdda05e3086b4256f8677c671f7b1d27a6976fa011d3fd0a86e", size = 42747508, upload-time = "2025-07-18T00:57:13.917Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b7/ba252f399bbf3addc731e8643c05532cf32e74cebb5e32f8f7409bc243cf/pyarrow-21.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:067c66ca29aaedae08218569a114e413b26e742171f526e828e1064fcdec13f4", size = 43345293, upload-time = "2025-07-18T00:57:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0a/a20819795bd702b9486f536a8eeb70a6aa64046fce32071c19ec8230dbaa/pyarrow-21.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0c4e75d13eb76295a49e0ea056eb18dbd87d81450bfeb8afa19a7e5a75ae2ad7", size = 45060670, upload-time = "2025-07-18T00:57:24.477Z" }, + { url = "https://files.pythonhosted.org/packages/10/15/6b30e77872012bbfe8265d42a01d5b3c17ef0ac0f2fae531ad91b6a6c02e/pyarrow-21.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdc4c17afda4dab2a9c0b79148a43a7f4e1094916b3e18d8975bfd6d6d52241f", size = 26227521, upload-time = "2025-07-18T00:57:29.119Z" }, +] + +[[package]] +name = "pyarrow" +version = "22.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151, upload-time = "2025-10-24T12:30:00.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/9b/cb3f7e0a345353def531ca879053e9ef6b9f38ed91aebcf68b09ba54dec0/pyarrow-22.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:77718810bd3066158db1e95a63c160ad7ce08c6b0710bc656055033e39cdad88", size = 34223968, upload-time = "2025-10-24T10:03:31.21Z" }, + { url = "https://files.pythonhosted.org/packages/6c/41/3184b8192a120306270c5307f105b70320fdaa592c99843c5ef78aaefdcf/pyarrow-22.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:44d2d26cda26d18f7af7db71453b7b783788322d756e81730acb98f24eb90ace", size = 35942085, upload-time = "2025-10-24T10:03:38.146Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3d/a1eab2f6f08001f9fb714b8ed5cfb045e2fe3e3e3c0c221f2c9ed1e6d67d/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b9d71701ce97c95480fecb0039ec5bb889e75f110da72005743451339262f4ce", size = 44964613, upload-time = "2025-10-24T10:03:46.516Z" }, + { url = "https://files.pythonhosted.org/packages/46/46/a1d9c24baf21cfd9ce994ac820a24608decf2710521b29223d4334985127/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:710624ab925dc2b05a6229d47f6f0dac1c1155e6ed559be7109f684eba048a48", size = 47627059, upload-time = "2025-10-24T10:03:55.353Z" }, + { url = "https://files.pythonhosted.org/packages/3a/4c/f711acb13075c1391fd54bc17e078587672c575f8de2a6e62509af026dcf/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f963ba8c3b0199f9d6b794c90ec77545e05eadc83973897a4523c9e8d84e9340", size = 47947043, upload-time = "2025-10-24T10:04:05.408Z" }, + { url = "https://files.pythonhosted.org/packages/4e/70/1f3180dd7c2eab35c2aca2b29ace6c519f827dcd4cfeb8e0dca41612cf7a/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd0d42297ace400d8febe55f13fdf46e86754842b860c978dfec16f081e5c653", size = 50206505, upload-time = "2025-10-24T10:04:15.786Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/fea6578112c8c60ffde55883a571e4c4c6bc7049f119d6b09333b5cc6f73/pyarrow-22.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:00626d9dc0f5ef3a75fe63fd68b9c7c8302d2b5bbc7f74ecaedba83447a24f84", size = 28101641, upload-time = "2025-10-24T10:04:22.57Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b7/18f611a8cdc43417f9394a3ccd3eace2f32183c08b9eddc3d17681819f37/pyarrow-22.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3e294c5eadfb93d78b0763e859a0c16d4051fc1c5231ae8956d61cb0b5666f5a", size = 34272022, upload-time = "2025-10-24T10:04:28.973Z" }, + { url = "https://files.pythonhosted.org/packages/26/5c/f259e2526c67eb4b9e511741b19870a02363a47a35edbebc55c3178db22d/pyarrow-22.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:69763ab2445f632d90b504a815a2a033f74332997052b721002298ed6de40f2e", size = 35995834, upload-time = "2025-10-24T10:04:35.467Z" }, + { url = "https://files.pythonhosted.org/packages/50/8d/281f0f9b9376d4b7f146913b26fac0aa2829cd1ee7e997f53a27411bbb92/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b41f37cabfe2463232684de44bad753d6be08a7a072f6a83447eeaf0e4d2a215", size = 45030348, upload-time = "2025-10-24T10:04:43.366Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e5/53c0a1c428f0976bf22f513d79c73000926cb00b9c138d8e02daf2102e18/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35ad0f0378c9359b3f297299c3309778bb03b8612f987399a0333a560b43862d", size = 47699480, upload-time = "2025-10-24T10:04:51.486Z" }, + { url = "https://files.pythonhosted.org/packages/95/e1/9dbe4c465c3365959d183e6345d0a8d1dc5b02ca3f8db4760b3bc834cf25/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8382ad21458075c2e66a82a29d650f963ce51c7708c7c0ff313a8c206c4fd5e8", size = 48011148, upload-time = "2025-10-24T10:04:59.585Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b4/7caf5d21930061444c3cf4fa7535c82faf5263e22ce43af7c2759ceb5b8b/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a812a5b727bc09c3d7ea072c4eebf657c2f7066155506ba31ebf4792f88f016", size = 50276964, upload-time = "2025-10-24T10:05:08.175Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f3/cec89bd99fa3abf826f14d4e53d3d11340ce6f6af4d14bdcd54cd83b6576/pyarrow-22.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ec5d40dd494882704fb876c16fa7261a69791e784ae34e6b5992e977bd2e238c", size = 28106517, upload-time = "2025-10-24T10:05:14.314Z" }, + { url = "https://files.pythonhosted.org/packages/af/63/ba23862d69652f85b615ca14ad14f3bcfc5bf1b99ef3f0cd04ff93fdad5a/pyarrow-22.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bea79263d55c24a32b0d79c00a1c58bb2ee5f0757ed95656b01c0fb310c5af3d", size = 34211578, upload-time = "2025-10-24T10:05:21.583Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d0/f9ad86fe809efd2bcc8be32032fa72e8b0d112b01ae56a053006376c5930/pyarrow-22.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:12fe549c9b10ac98c91cf791d2945e878875d95508e1a5d14091a7aaa66d9cf8", size = 35989906, upload-time = "2025-10-24T10:05:29.485Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a8/f910afcb14630e64d673f15904ec27dd31f1e009b77033c365c84e8c1e1d/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:334f900ff08ce0423407af97e6c26ad5d4e3b0763645559ece6fbf3747d6a8f5", size = 45021677, upload-time = "2025-10-24T10:05:38.274Z" }, + { url = "https://files.pythonhosted.org/packages/13/95/aec81f781c75cd10554dc17a25849c720d54feafb6f7847690478dcf5ef8/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c6c791b09c57ed76a18b03f2631753a4960eefbbca80f846da8baefc6491fcfe", size = 47726315, upload-time = "2025-10-24T10:05:47.314Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d4/74ac9f7a54cfde12ee42734ea25d5a3c9a45db78f9def949307a92720d37/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c3200cb41cdbc65156e5f8c908d739b0dfed57e890329413da2748d1a2cd1a4e", size = 47990906, upload-time = "2025-10-24T10:05:58.254Z" }, + { url = "https://files.pythonhosted.org/packages/2e/71/fedf2499bf7a95062eafc989ace56572f3343432570e1c54e6599d5b88da/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ac93252226cf288753d8b46280f4edf3433bf9508b6977f8dd8526b521a1bbb9", size = 50306783, upload-time = "2025-10-24T10:06:08.08Z" }, + { url = "https://files.pythonhosted.org/packages/68/ed/b202abd5a5b78f519722f3d29063dda03c114711093c1995a33b8e2e0f4b/pyarrow-22.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:44729980b6c50a5f2bfcc2668d36c569ce17f8b17bccaf470c4313dcbbf13c9d", size = 27972883, upload-time = "2025-10-24T10:06:14.204Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d6/d0fac16a2963002fc22c8fa75180a838737203d558f0ed3b564c4a54eef5/pyarrow-22.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6e95176209257803a8b3d0394f21604e796dadb643d2f7ca21b66c9c0b30c9a", size = 34204629, upload-time = "2025-10-24T10:06:20.274Z" }, + { url = "https://files.pythonhosted.org/packages/c6/9c/1d6357347fbae062ad3f17082f9ebc29cc733321e892c0d2085f42a2212b/pyarrow-22.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:001ea83a58024818826a9e3f89bf9310a114f7e26dfe404a4c32686f97bd7901", size = 35985783, upload-time = "2025-10-24T10:06:27.301Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/782344c2ce58afbea010150df07e3a2f5fdad299cd631697ae7bd3bac6e3/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ce20fe000754f477c8a9125543f1936ea5b8867c5406757c224d745ed033e691", size = 45020999, upload-time = "2025-10-24T10:06:35.387Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8b/5362443737a5307a7b67c1017c42cd104213189b4970bf607e05faf9c525/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e0a15757fccb38c410947df156f9749ae4a3c89b2393741a50521f39a8cf202a", size = 47724601, upload-time = "2025-10-24T10:06:43.551Z" }, + { url = "https://files.pythonhosted.org/packages/69/4d/76e567a4fc2e190ee6072967cb4672b7d9249ac59ae65af2d7e3047afa3b/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cedb9dd9358e4ea1d9bce3665ce0797f6adf97ff142c8e25b46ba9cdd508e9b6", size = 48001050, upload-time = "2025-10-24T10:06:52.284Z" }, + { url = "https://files.pythonhosted.org/packages/01/5e/5653f0535d2a1aef8223cee9d92944cb6bccfee5cf1cd3f462d7cb022790/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:252be4a05f9d9185bb8c18e83764ebcfea7185076c07a7a662253af3a8c07941", size = 50307877, upload-time = "2025-10-24T10:07:02.405Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f8/1d0bd75bf9328a3b826e24a16e5517cd7f9fbf8d34a3184a4566ef5a7f29/pyarrow-22.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:a4893d31e5ef780b6edcaf63122df0f8d321088bb0dee4c8c06eccb1ca28d145", size = 27977099, upload-time = "2025-10-24T10:08:07.259Z" }, + { url = "https://files.pythonhosted.org/packages/90/81/db56870c997805bf2b0f6eeeb2d68458bf4654652dccdcf1bf7a42d80903/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:f7fe3dbe871294ba70d789be16b6e7e52b418311e166e0e3cba9522f0f437fb1", size = 34336685, upload-time = "2025-10-24T10:07:11.47Z" }, + { url = "https://files.pythonhosted.org/packages/1c/98/0727947f199aba8a120f47dfc229eeb05df15bcd7a6f1b669e9f882afc58/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ba95112d15fd4f1105fb2402c4eab9068f0554435e9b7085924bcfaac2cc306f", size = 36032158, upload-time = "2025-10-24T10:07:18.626Z" }, + { url = "https://files.pythonhosted.org/packages/96/b4/9babdef9c01720a0785945c7cf550e4acd0ebcd7bdd2e6f0aa7981fa85e2/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c064e28361c05d72eed8e744c9605cbd6d2bb7481a511c74071fd9b24bc65d7d", size = 44892060, upload-time = "2025-10-24T10:07:26.002Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ca/2f8804edd6279f78a37062d813de3f16f29183874447ef6d1aadbb4efa0f/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6f9762274496c244d951c819348afbcf212714902742225f649cf02823a6a10f", size = 47504395, upload-time = "2025-10-24T10:07:34.09Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f0/77aa5198fd3943682b2e4faaf179a674f0edea0d55d326d83cb2277d9363/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a9d9ffdc2ab696f6b15b4d1f7cec6658e1d788124418cb30030afbae31c64746", size = 48066216, upload-time = "2025-10-24T10:07:43.528Z" }, + { url = "https://files.pythonhosted.org/packages/79/87/a1937b6e78b2aff18b706d738c9e46ade5bfcf11b294e39c87706a0089ac/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ec1a15968a9d80da01e1d30349b2b0d7cc91e96588ee324ce1b5228175043e95", size = 50288552, upload-time = "2025-10-24T10:07:53.519Z" }, + { url = "https://files.pythonhosted.org/packages/60/ae/b5a5811e11f25788ccfdaa8f26b6791c9807119dffcf80514505527c384c/pyarrow-22.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bba208d9c7decf9961998edf5c65e3ea4355d5818dd6cd0f6809bec1afb951cc", size = 28262504, upload-time = "2025-10-24T10:08:00.932Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b0/0fa4d28a8edb42b0a7144edd20befd04173ac79819547216f8a9f36f9e50/pyarrow-22.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9bddc2cade6561f6820d4cd73f99a0243532ad506bc510a75a5a65a522b2d74d", size = 34224062, upload-time = "2025-10-24T10:08:14.101Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a8/7a719076b3c1be0acef56a07220c586f25cd24de0e3f3102b438d18ae5df/pyarrow-22.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e70ff90c64419709d38c8932ea9fe1cc98415c4f87ea8da81719e43f02534bc9", size = 35990057, upload-time = "2025-10-24T10:08:21.842Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/359ed54c93b47fb6fe30ed16cdf50e3f0e8b9ccfb11b86218c3619ae50a8/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:92843c305330aa94a36e706c16209cd4df274693e777ca47112617db7d0ef3d7", size = 45068002, upload-time = "2025-10-24T10:08:29.034Z" }, + { url = "https://files.pythonhosted.org/packages/55/fc/4945896cc8638536ee787a3bd6ce7cec8ec9acf452d78ec39ab328efa0a1/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:6dda1ddac033d27421c20d7a7943eec60be44e0db4e079f33cc5af3b8280ccde", size = 47737765, upload-time = "2025-10-24T10:08:38.559Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5e/7cb7edeb2abfaa1f79b5d5eb89432356155c8426f75d3753cbcb9592c0fd/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:84378110dd9a6c06323b41b56e129c504d157d1a983ce8f5443761eb5256bafc", size = 48048139, upload-time = "2025-10-24T10:08:46.784Z" }, + { url = "https://files.pythonhosted.org/packages/88/c6/546baa7c48185f5e9d6e59277c4b19f30f48c94d9dd938c2a80d4d6b067c/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:854794239111d2b88b40b6ef92aa478024d1e5074f364033e73e21e3f76b25e0", size = 50314244, upload-time = "2025-10-24T10:08:55.771Z" }, + { url = "https://files.pythonhosted.org/packages/3c/79/755ff2d145aafec8d347bf18f95e4e81c00127f06d080135dfc86aea417c/pyarrow-22.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:b883fe6fd85adad7932b3271c38ac289c65b7337c2c132e9569f9d3940620730", size = 28757501, upload-time = "2025-10-24T10:09:59.891Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d2/237d75ac28ced3147912954e3c1a174df43a95f4f88e467809118a8165e0/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7a820d8ae11facf32585507c11f04e3f38343c1e784c9b5a8b1da5c930547fe2", size = 34355506, upload-time = "2025-10-24T10:09:02.953Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/733dfffe6d3069740f98e57ff81007809067d68626c5faef293434d11bd6/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:c6ec3675d98915bf1ec8b3c7986422682f7232ea76cad276f4c8abd5b7319b70", size = 36047312, upload-time = "2025-10-24T10:09:10.334Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2b/29d6e3782dc1f299727462c1543af357a0f2c1d3c160ce199950d9ca51eb/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3e739edd001b04f654b166204fc7a9de896cf6007eaff33409ee9e50ceaff754", size = 45081609, upload-time = "2025-10-24T10:09:18.61Z" }, + { url = "https://files.pythonhosted.org/packages/8d/42/aa9355ecc05997915af1b7b947a7f66c02dcaa927f3203b87871c114ba10/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7388ac685cab5b279a41dfe0a6ccd99e4dbf322edfb63e02fc0443bf24134e91", size = 47703663, upload-time = "2025-10-24T10:09:27.369Z" }, + { url = "https://files.pythonhosted.org/packages/ee/62/45abedde480168e83a1de005b7b7043fd553321c1e8c5a9a114425f64842/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f633074f36dbc33d5c05b5dc75371e5660f1dbf9c8b1d95669def05e5425989c", size = 48066543, upload-time = "2025-10-24T10:09:34.908Z" }, + { url = "https://files.pythonhosted.org/packages/84/e9/7878940a5b072e4f3bf998770acafeae13b267f9893af5f6d4ab3904b67e/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4c19236ae2402a8663a2c8f21f1870a03cc57f0bef7e4b6eb3238cc82944de80", size = 50288838, upload-time = "2025-10-24T10:09:44.394Z" }, + { url = "https://files.pythonhosted.org/packages/7b/03/f335d6c52b4a4761bcc83499789a1e2e16d9d201a58c327a9b5cc9a41bd9/pyarrow-22.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0c34fe18094686194f204a3b1787a27456897d8a2d62caf84b61e8dfbc0252ae", size = 29185594, upload-time = "2025-10-24T10:09:53.111Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pluggy", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pluggy", marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-benchmark" +version = "5.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "py-cpuinfo" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/34/9f732b76456d64faffbef6232f1f9dbec7a7c4999ff46282fa418bd1af66/pytest_benchmark-5.2.3.tar.gz", hash = "sha256:deb7317998a23c650fd4ff76e1230066a76cb45dcece0aca5607143c619e7779", size = 341340, upload-time = "2025-11-09T18:48:43.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-py3-none-any.whl", hash = "sha256:bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803", size = 45255, upload-time = "2025-11-09T18:48:39.765Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "filelock", version = "3.25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "platformdirs", version = "4.9.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/bb/93a3e83bdf9322c7e21cafd092e56a4a17c4d8ef4277b6eb01af1a540a6f/python_discovery-1.1.0.tar.gz", hash = "sha256:447941ba1aed8cc2ab7ee3cb91be5fc137c5bdbb05b7e6ea62fbdcb66e50b268", size = 55674, upload-time = "2026-02-26T09:42:49.668Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/54/82a6e2ef37f0f23dccac604b9585bdcbd0698604feb64807dcb72853693e/python_discovery-1.1.0-py3-none-any.whl", hash = "sha256:a162893b8809727f54594a99ad2179d2ede4bf953e12d4c7abc3cc9cdbd1437b", size = 30687, upload-time = "2026-02-26T09:42:48.548Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/59/42/b86689aac0cdaee7ae1c58d464b0ff04ca909c19bb6502d4973cdd9f9544/pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b", size = 8760837, upload-time = "2025-07-14T20:12:59.59Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8a/1403d0353f8c5a2f0829d2b1c4becbf9da2f0a4d040886404fc4a5431e4d/pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91", size = 9590187, upload-time = "2025-07-14T20:13:01.419Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/e0e8d802f124772cec9c75430b01a212f86f9de7546bda715e54140d5aeb/pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d", size = 8778162, upload-time = "2025-07-14T20:13:03.544Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, + { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/b9/52aa9ec2867528b54f1e60846728d8b4d84726630874fee3a91e66c7df81/pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4", size = 1329850, upload-time = "2025-09-08T23:07:26.274Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/5653e7b7425b169f994835a2b2abf9486264401fdef18df91ddae47ce2cc/pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556", size = 906380, upload-time = "2025-09-08T23:07:29.78Z" }, + { url = "https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b", size = 666421, upload-time = "2025-09-08T23:07:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e", size = 854149, upload-time = "2025-09-08T23:07:33.17Z" }, + { url = "https://files.pythonhosted.org/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526", size = 1655070, upload-time = "2025-09-08T23:07:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1", size = 2033441, upload-time = "2025-09-08T23:07:37.432Z" }, + { url = "https://files.pythonhosted.org/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386", size = 1891529, upload-time = "2025-09-08T23:07:39.047Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3e/154fb963ae25be70c0064ce97776c937ecc7d8b0259f22858154a9999769/pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda", size = 567276, upload-time = "2025-09-08T23:07:40.695Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/f4ab56c8c595abcb26b2be5fd9fa9e6899c1e5ad54964e93ae8bb35482be/pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f", size = 632208, upload-time = "2025-09-08T23:07:42.298Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e3/be2cc7ab8332bdac0522fdb64c17b1b6241a795bee02e0196636ec5beb79/pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32", size = 559766, upload-time = "2025-09-08T23:07:43.869Z" }, + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4e/782eb6df91b6a9d9afa96c2dcfc5cac62562a68eb62a02210101f886014d/pyzmq-27.1.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:96c71c32fff75957db6ae33cd961439f386505c6e6b377370af9b24a1ef9eafb", size = 1330426, upload-time = "2025-09-08T23:09:21.03Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ca/2b8693d06b1db4e0c084871e4c9d7842b561d0a6ff9d780640f5e3e9eb55/pyzmq-27.1.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:49d3980544447f6bd2968b6ac913ab963a49dcaa2d4a2990041f16057b04c429", size = 906559, upload-time = "2025-09-08T23:09:22.983Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b3/b99b39e2cfdcebd512959780e4d299447fd7f46010b1d88d63324e2481ec/pyzmq-27.1.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:849ca054d81aa1c175c49484afaaa5db0622092b5eccb2055f9f3bb8f703782d", size = 863816, upload-time = "2025-09-08T23:09:24.556Z" }, + { url = "https://files.pythonhosted.org/packages/61/b2/018fa8e8eefb34a625b1a45e2effcbc9885645b22cdd0a68283f758351e7/pyzmq-27.1.0-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3970778e74cb7f85934d2b926b9900e92bfe597e62267d7499acc39c9c28e345", size = 666735, upload-time = "2025-09-08T23:09:26.297Z" }, + { url = "https://files.pythonhosted.org/packages/01/05/8ae778f7cd7c94030731ae2305e6a38f3a333b6825f56c0c03f2134ccf1b/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:da96ecdcf7d3919c3be2de91a8c513c186f6762aa6cf7c01087ed74fad7f0968", size = 1655425, upload-time = "2025-09-08T23:09:28.172Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ad/d69478a97a3f3142f9dbbbd9daa4fcf42541913a85567c36d4cfc19b2218/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9541c444cfe1b1c0156c5c86ece2bb926c7079a18e7b47b0b1b3b1b875e5d098", size = 2033729, upload-time = "2025-09-08T23:09:30.097Z" }, + { url = "https://files.pythonhosted.org/packages/9a/6d/e3c6ad05bc1cddd25094e66cc15ae8924e15c67e231e93ed2955c401007e/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e30a74a39b93e2e1591b58eb1acef4902be27c957a8720b0e368f579b82dc22f", size = 1891803, upload-time = "2025-09-08T23:09:31.875Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a7/97e8be0daaca157511563160b67a13d4fe76b195e3fa6873cb554ad46be3/pyzmq-27.1.0-cp39-cp39-win32.whl", hash = "sha256:b1267823d72d1e40701dcba7edc45fd17f71be1285557b7fe668887150a14b78", size = 567627, upload-time = "2025-09-08T23:09:33.98Z" }, + { url = "https://files.pythonhosted.org/packages/5c/91/70bbf3a7c5b04c904261ef5ba224d8a76315f6c23454251bf5f55573a8a1/pyzmq-27.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c996ded912812a2fcd7ab6574f4ad3edc27cb6510349431e4930d4196ade7db", size = 632315, upload-time = "2025-09-08T23:09:36.097Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b5/a4173a83c7fd37f6bdb5a800ea338bc25603284e9ef8681377cec006ede4/pyzmq-27.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:346e9ba4198177a07e7706050f35d733e08c1c1f8ceacd5eb6389d653579ffbc", size = 559833, upload-time = "2025-09-08T23:09:38.183Z" }, + { url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266, upload-time = "2025-09-08T23:09:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" }, + { url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74", size = 747371, upload-time = "2025-09-08T23:09:45.575Z" }, + { url = "https://files.pythonhosted.org/packages/26/af/78ce193dbf03567eb8c0dc30e3df2b9e56f12a670bf7eb20f9fb532c7e8a/pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba", size = 544862, upload-time = "2025-09-08T23:09:47.448Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, + { url = "https://files.pythonhosted.org/packages/57/f4/c2e978cf6b833708bad7d6396c3a20c19750585a1775af3ff13c435e1912/pyzmq-27.1.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:722ea791aa233ac0a819fc2c475e1292c76930b31f1d828cb61073e2fe5e208f", size = 836257, upload-time = "2025-09-08T23:10:07.635Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5f/4e10c7f57a4c92ab0fbb2396297aa8d618e6f5b9b8f8e9756d56f3e6fc52/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:01f9437501886d3a1dd4b02ef59fb8cc384fa718ce066d52f175ee49dd5b7ed8", size = 800203, upload-time = "2025-09-08T23:10:09.436Z" }, + { url = "https://files.pythonhosted.org/packages/19/72/a74a007cd636f903448c6ab66628104b1fc5f2ba018733d5eabb94a0a6fb/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4a19387a3dddcc762bfd2f570d14e2395b2c9701329b266f83dd87a2b3cbd381", size = 758756, upload-time = "2025-09-08T23:10:11.733Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d4/30c25b91f2b4786026372f5ef454134d7f576fcf4ac58539ad7dd5de4762/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c618fbcd069e3a29dcd221739cacde52edcc681f041907867e0f5cc7e85f172", size = 567742, upload-time = "2025-09-08T23:10:14.732Z" }, + { url = "https://files.pythonhosted.org/packages/92/aa/ee86edad943438cd0316964020c4b6d09854414f9f945f8e289ea6fcc019/pyzmq-27.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff8d114d14ac671d88c89b9224c63d6c4e5a613fe8acd5594ce53d752a3aafe9", size = 544857, upload-time = "2025-09-08T23:10:16.431Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/1b/ab712a9d5044435be8e9a2beb17cbfa4c241aa9b5e4413febac2a8b79ef2/ruff-0.14.9.tar.gz", hash = "sha256:35f85b25dd586381c0cc053f48826109384c81c00ad7ef1bd977bfcc28119d5b", size = 5809165, upload-time = "2025-12-11T21:39:47.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/1c/d1b1bba22cffec02351c78ab9ed4f7d7391876e12720298448b29b7229c1/ruff-0.14.9-py3-none-linux_armv6l.whl", hash = "sha256:f1ec5de1ce150ca6e43691f4a9ef5c04574ad9ca35c8b3b0e18877314aba7e75", size = 13576541, upload-time = "2025-12-11T21:39:14.806Z" }, + { url = "https://files.pythonhosted.org/packages/94/ab/ffe580e6ea1fca67f6337b0af59fc7e683344a43642d2d55d251ff83ceae/ruff-0.14.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ed9d7417a299fc6030b4f26333bf1117ed82a61ea91238558c0268c14e00d0c2", size = 13779363, upload-time = "2025-12-11T21:39:20.29Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f8/2be49047f929d6965401855461e697ab185e1a6a683d914c5c19c7962d9e/ruff-0.14.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d5dc3473c3f0e4a1008d0ef1d75cee24a48e254c8bed3a7afdd2b4392657ed2c", size = 12925292, upload-time = "2025-12-11T21:39:38.757Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/08840ff5127916bb989c86f18924fd568938b06f58b60e206176f327c0fe/ruff-0.14.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84bf7c698fc8f3cb8278830fb6b5a47f9bcc1ed8cb4f689b9dd02698fa840697", size = 13362894, upload-time = "2025-12-11T21:39:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/31/1c/5b4e8e7750613ef43390bb58658eaf1d862c0cc3352d139cd718a2cea164/ruff-0.14.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa733093d1f9d88a5d98988d8834ef5d6f9828d03743bf5e338bf980a19fce27", size = 13311482, upload-time = "2025-12-11T21:39:17.51Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3a/459dce7a8cb35ba1ea3e9c88f19077667a7977234f3b5ab197fad240b404/ruff-0.14.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a1cfb04eda979b20c8c19550c8b5f498df64ff8da151283311ce3199e8b3648", size = 14016100, upload-time = "2025-12-11T21:39:41.948Z" }, + { url = "https://files.pythonhosted.org/packages/a6/31/f064f4ec32524f9956a0890fc6a944e5cf06c63c554e39957d208c0ffc45/ruff-0.14.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1e5cb521e5ccf0008bd74d5595a4580313844a42b9103b7388eca5a12c970743", size = 15477729, upload-time = "2025-12-11T21:39:23.279Z" }, + { url = "https://files.pythonhosted.org/packages/7a/6d/f364252aad36ccd443494bc5f02e41bf677f964b58902a17c0b16c53d890/ruff-0.14.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd429a8926be6bba4befa8cdcf3f4dd2591c413ea5066b1e99155ed245ae42bb", size = 15122386, upload-time = "2025-12-11T21:39:33.125Z" }, + { url = "https://files.pythonhosted.org/packages/20/02/e848787912d16209aba2799a4d5a1775660b6a3d0ab3944a4ccc13e64a02/ruff-0.14.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab208c1b7a492e37caeaf290b1378148f75e13c2225af5d44628b95fd7834273", size = 14497124, upload-time = "2025-12-11T21:38:59.33Z" }, + { url = "https://files.pythonhosted.org/packages/f3/51/0489a6a5595b7760b5dbac0dd82852b510326e7d88d51dbffcd2e07e3ff3/ruff-0.14.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72034534e5b11e8a593f517b2f2f2b273eb68a30978c6a2d40473ad0aaa4cb4a", size = 14195343, upload-time = "2025-12-11T21:39:44.866Z" }, + { url = "https://files.pythonhosted.org/packages/f6/53/3bb8d2fa73e4c2f80acc65213ee0830fa0c49c6479313f7a68a00f39e208/ruff-0.14.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:712ff04f44663f1b90a1195f51525836e3413c8a773574a7b7775554269c30ed", size = 14346425, upload-time = "2025-12-11T21:39:05.927Z" }, + { url = "https://files.pythonhosted.org/packages/ad/04/bdb1d0ab876372da3e983896481760867fc84f969c5c09d428e8f01b557f/ruff-0.14.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a111fee1db6f1d5d5810245295527cda1d367c5aa8f42e0fca9a78ede9b4498b", size = 13258768, upload-time = "2025-12-11T21:39:08.691Z" }, + { url = "https://files.pythonhosted.org/packages/40/d9/8bf8e1e41a311afd2abc8ad12be1b6c6c8b925506d9069b67bb5e9a04af3/ruff-0.14.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8769efc71558fecc25eb295ddec7d1030d41a51e9dcf127cbd63ec517f22d567", size = 13326939, upload-time = "2025-12-11T21:39:53.842Z" }, + { url = "https://files.pythonhosted.org/packages/f4/56/a213fa9edb6dd849f1cfbc236206ead10913693c72a67fb7ddc1833bf95d/ruff-0.14.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:347e3bf16197e8a2de17940cd75fd6491e25c0aa7edf7d61aa03f146a1aa885a", size = 13578888, upload-time = "2025-12-11T21:39:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/33/09/6a4a67ffa4abae6bf44c972a4521337ffce9cbc7808faadede754ef7a79c/ruff-0.14.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7715d14e5bccf5b660f54516558aa94781d3eb0838f8e706fb60e3ff6eff03a8", size = 14314473, upload-time = "2025-12-11T21:39:50.78Z" }, + { url = "https://files.pythonhosted.org/packages/12/0d/15cc82da5d83f27a3c6b04f3a232d61bc8c50d38a6cd8da79228e5f8b8d6/ruff-0.14.9-py3-none-win32.whl", hash = "sha256:df0937f30aaabe83da172adaf8937003ff28172f59ca9f17883b4213783df197", size = 13202651, upload-time = "2025-12-11T21:39:26.628Z" }, + { url = "https://files.pythonhosted.org/packages/32/f7/c78b060388eefe0304d9d42e68fab8cffd049128ec466456cef9b8d4f06f/ruff-0.14.9-py3-none-win_amd64.whl", hash = "sha256:c0b53a10e61df15a42ed711ec0bda0c582039cf6c754c49c020084c55b5b0bc2", size = 14702079, upload-time = "2025-12-11T21:39:11.954Z" }, + { url = "https://files.pythonhosted.org/packages/26/09/7a9520315decd2334afa65ed258fed438f070e31f05a2e43dd480a5e5911/ruff-0.14.9-py3-none-win_arm64.whl", hash = "sha256:8e821c366517a074046d92f0e9213ed1c13dbc5b37a7fc20b07f79b64d62cc84", size = 13744730, upload-time = "2025-12-11T21:39:29.659Z" }, +] + +[[package]] +name = "scipy" +version = "1.13.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/00/48c2f661e2816ccf2ecd77982f6605b2950afe60f60a52b4cbbc2504aa8f/scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c", size = 57210720, upload-time = "2024-05-23T03:29:26.079Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/59/41b2529908c002ade869623b87eecff3e11e3ce62e996d0bdcb536984187/scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca", size = 39328076, upload-time = "2024-05-23T03:19:01.687Z" }, + { url = "https://files.pythonhosted.org/packages/d5/33/f1307601f492f764062ce7dd471a14750f3360e33cd0f8c614dae208492c/scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f", size = 30306232, upload-time = "2024-05-23T03:19:09.089Z" }, + { url = "https://files.pythonhosted.org/packages/c0/66/9cd4f501dd5ea03e4a4572ecd874936d0da296bd04d1c45ae1a4a75d9c3a/scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989", size = 33743202, upload-time = "2024-05-23T03:19:15.138Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ba/7255e5dc82a65adbe83771c72f384d99c43063648456796436c9a5585ec3/scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f", size = 38577335, upload-time = "2024-05-23T03:19:21.984Z" }, + { url = "https://files.pythonhosted.org/packages/49/a5/bb9ded8326e9f0cdfdc412eeda1054b914dfea952bda2097d174f8832cc0/scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94", size = 38820728, upload-time = "2024-05-23T03:19:28.225Z" }, + { url = "https://files.pythonhosted.org/packages/12/30/df7a8fcc08f9b4a83f5f27cfaaa7d43f9a2d2ad0b6562cced433e5b04e31/scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54", size = 46210588, upload-time = "2024-05-23T03:19:35.661Z" }, + { url = "https://files.pythonhosted.org/packages/b4/15/4a4bb1b15bbd2cd2786c4f46e76b871b28799b67891f23f455323a0cdcfb/scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9", size = 39333805, upload-time = "2024-05-23T03:19:43.081Z" }, + { url = "https://files.pythonhosted.org/packages/ba/92/42476de1af309c27710004f5cdebc27bec62c204db42e05b23a302cb0c9a/scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326", size = 30317687, upload-time = "2024-05-23T03:19:48.799Z" }, + { url = "https://files.pythonhosted.org/packages/80/ba/8be64fe225360a4beb6840f3cbee494c107c0887f33350d0a47d55400b01/scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299", size = 33694638, upload-time = "2024-05-23T03:19:55.104Z" }, + { url = "https://files.pythonhosted.org/packages/36/07/035d22ff9795129c5a847c64cb43c1fa9188826b59344fee28a3ab02e283/scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa", size = 38569931, upload-time = "2024-05-23T03:20:01.82Z" }, + { url = "https://files.pythonhosted.org/packages/d9/10/f9b43de37e5ed91facc0cfff31d45ed0104f359e4f9a68416cbf4e790241/scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59", size = 38838145, upload-time = "2024-05-23T03:20:09.173Z" }, + { url = "https://files.pythonhosted.org/packages/4a/48/4513a1a5623a23e95f94abd675ed91cfb19989c58e9f6f7d03990f6caf3d/scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b", size = 46196227, upload-time = "2024-05-23T03:20:16.433Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7b/fb6b46fbee30fc7051913068758414f2721003a89dd9a707ad49174e3843/scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1", size = 39357301, upload-time = "2024-05-23T03:20:23.538Z" }, + { url = "https://files.pythonhosted.org/packages/dc/5a/2043a3bde1443d94014aaa41e0b50c39d046dda8360abd3b2a1d3f79907d/scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d", size = 30363348, upload-time = "2024-05-23T03:20:29.885Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cb/26e4a47364bbfdb3b7fb3363be6d8a1c543bcd70a7753ab397350f5f189a/scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627", size = 33406062, upload-time = "2024-05-23T03:20:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/88/ab/6ecdc526d509d33814835447bbbeedbebdec7cca46ef495a61b00a35b4bf/scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884", size = 38218311, upload-time = "2024-05-23T03:20:42.086Z" }, + { url = "https://files.pythonhosted.org/packages/0b/00/9f54554f0f8318100a71515122d8f4f503b1a2c4b4cfab3b4b68c0eb08fa/scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16", size = 38442493, upload-time = "2024-05-23T03:20:48.292Z" }, + { url = "https://files.pythonhosted.org/packages/3e/df/963384e90733e08eac978cd103c34df181d1fec424de383cdc443f418dd4/scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949", size = 45910955, upload-time = "2024-05-23T03:20:55.091Z" }, + { url = "https://files.pythonhosted.org/packages/7f/29/c2ea58c9731b9ecb30b6738113a95d147e83922986b34c685b8f6eefde21/scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5", size = 39352927, upload-time = "2024-05-23T03:21:01.95Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c0/e71b94b20ccf9effb38d7147c0064c08c622309fd487b1b677771a97d18c/scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24", size = 30324538, upload-time = "2024-05-23T03:21:07.634Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0f/aaa55b06d474817cea311e7b10aab2ea1fd5d43bc6a2861ccc9caec9f418/scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004", size = 33732190, upload-time = "2024-05-23T03:21:14.41Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/d0ad1a96f80962ba65e2ce1de6a1e59edecd1f0a7b55990ed208848012e0/scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d", size = 38612244, upload-time = "2024-05-23T03:21:21.827Z" }, + { url = "https://files.pythonhosted.org/packages/8d/02/1165905f14962174e6569076bcc3315809ae1291ed14de6448cc151eedfd/scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c", size = 38845637, upload-time = "2024-05-23T03:21:28.729Z" }, + { url = "https://files.pythonhosted.org/packages/3e/77/dab54fe647a08ee4253963bcd8f9cf17509c8ca64d6335141422fe2e2114/scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2", size = 46227440, upload-time = "2024-05-23T03:21:35.888Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "seaborn" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "matplotlib", version = "3.10.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/1d/0a336abf618272d53f62ebe274f712e213f5a03c0b2339575430b8362ef2/tornado-6.5.4.tar.gz", hash = "sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7", size = 513632, upload-time = "2025-12-15T19:21:03.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9", size = 443909, upload-time = "2025-12-15T19:20:48.382Z" }, + { url = "https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843", size = 442163, upload-time = "2025-12-15T19:20:49.791Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b5/206f82d51e1bfa940ba366a8d2f83904b15942c45a78dd978b599870ab44/tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cf66105dc6acb5af613c054955b8137e34a03698aa53272dbda4afe252be17", size = 445746, upload-time = "2025-12-15T19:20:51.491Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9d/1a3338e0bd30ada6ad4356c13a0a6c35fbc859063fa7eddb309183364ac1/tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ff0a58b0dc97939d29da29cd624da010e7f804746621c78d14b80238669335", size = 445083, upload-time = "2025-12-15T19:20:52.778Z" }, + { url = "https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f", size = 445315, upload-time = "2025-12-15T19:20:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/27/07/2273972f69ca63dbc139694a3fc4684edec3ea3f9efabf77ed32483b875c/tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9c86b1643b33a4cd415f8d0fe53045f913bf07b4a3ef646b735a6a86047dda84", size = 446003, upload-time = "2025-12-15T19:20:56.101Z" }, + { url = "https://files.pythonhosted.org/packages/d1/83/41c52e47502bf7260044413b6770d1a48dda2f0246f95ee1384a3cd9c44a/tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:6eb82872335a53dd063a4f10917b3efd28270b56a33db69009606a0312660a6f", size = 445412, upload-time = "2025-12-15T19:20:57.398Z" }, + { url = "https://files.pythonhosted.org/packages/10/c7/bc96917f06cbee182d44735d4ecde9c432e25b84f4c2086143013e7b9e52/tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6076d5dda368c9328ff41ab5d9dd3608e695e8225d1cd0fd1e006f05da3635a8", size = 445392, upload-time = "2025-12-15T19:20:58.692Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1a/d7592328d037d36f2d2462f4bc1fbb383eec9278bc786c1b111cbbd44cfa/tornado-6.5.4-cp39-abi3-win32.whl", hash = "sha256:1768110f2411d5cd281bac0a090f707223ce77fd110424361092859e089b38d1", size = 446481, upload-time = "2025-12-15T19:21:00.008Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6d/c69be695a0a64fd37a97db12355a035a6d90f79067a3cf936ec2b1dc38cd/tornado-6.5.4-cp39-abi3-win_amd64.whl", hash = "sha256:fa07d31e0cd85c60713f2b995da613588aa03e1303d75705dca6af8babc18ddc", size = 446886, upload-time = "2025-12-15T19:21:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/50/49/8dc3fd90902f70084bd2cd059d576ddb4f8bb44c2c7c0e33a11422acb17e/tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1", size = 445910, upload-time = "2025-12-15T19:21:02.571Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "tracking-data-container" +source = { virtual = "." } + +[package.optional-dependencies] +all = [ + { name = "pandas" }, + { name = "polars" }, + { name = "pyarrow", version = "21.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyarrow", version = "22.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +pandas = [ + { name = "pandas" }, + { name = "pyarrow", version = "21.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyarrow", version = "22.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +polars = [ + { name = "polars" }, +] +pyarrow = [ + { name = "pyarrow", version = "21.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyarrow", version = "22.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[package.dev-dependencies] +all = [ + { name = "fast-forward-football", marker = "python_full_version == '3.11.*'" }, + { name = "huggingface-hub" }, + { name = "ipykernel", version = "6.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "ipykernel", version = "7.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "kloppy" }, + { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "matplotlib", version = "3.10.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "maturin" }, + { name = "mplsoccer", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "mplsoccer", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mypy" }, + { name = "pandas" }, + { name = "pandas-stubs", version = "2.2.2.240807", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pandas-stubs", version = "2.3.3.260113", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "pandas-stubs", version = "3.0.0.260204", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pdoc" }, + { name = "polars" }, + { name = "pre-commit", version = "4.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pre-commit", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyarrow", version = "21.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyarrow", version = "22.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest-benchmark" }, + { name = "ruff" }, +] +benchmarks = [ + { name = "fast-forward-football", marker = "python_full_version == '3.11.*'" }, + { name = "kloppy" }, + { name = "maturin" }, + { name = "pandas" }, + { name = "polars" }, + { name = "pre-commit", version = "4.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pre-commit", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyarrow", version = "21.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyarrow", version = "22.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest-benchmark" }, +] +dev = [ + { name = "maturin" }, + { name = "pandas" }, + { name = "polars" }, + { name = "pre-commit", version = "4.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pre-commit", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyarrow", version = "21.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyarrow", version = "22.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +docs = [ + { name = "maturin" }, + { name = "pandas" }, + { name = "pdoc" }, + { name = "polars" }, + { name = "pre-commit", version = "4.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pre-commit", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyarrow", version = "21.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyarrow", version = "22.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +linting = [ + { name = "maturin" }, + { name = "mypy" }, + { name = "pandas" }, + { name = "pandas-stubs", version = "2.2.2.240807", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pandas-stubs", version = "2.3.3.260113", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "pandas-stubs", version = "3.0.0.260204", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "polars" }, + { name = "pre-commit", version = "4.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pre-commit", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyarrow", version = "21.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyarrow", version = "22.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "ruff" }, +] +nb = [ + { name = "huggingface-hub" }, + { name = "ipykernel", version = "6.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "ipykernel", version = "7.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "matplotlib", version = "3.10.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mplsoccer", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "mplsoccer", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +testing = [ + { name = "maturin" }, + { name = "pandas" }, + { name = "polars" }, + { name = "pre-commit", version = "4.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pre-commit", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyarrow", version = "21.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyarrow", version = "22.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest-benchmark" }, +] + +[package.metadata] +requires-dist = [ + { name = "pandas", marker = "extra == 'all'", specifier = ">=1.4.4" }, + { name = "pandas", marker = "extra == 'pandas'", specifier = ">=1.4.4" }, + { name = "polars", marker = "extra == 'all'", specifier = ">=0.16.14" }, + { name = "polars", marker = "extra == 'polars'", specifier = ">=0.16.14" }, + { name = "pyarrow", marker = "extra == 'all'", specifier = ">=8.0.0" }, + { name = "pyarrow", marker = "extra == 'pandas'", specifier = ">=8.0.0" }, + { name = "pyarrow", marker = "extra == 'pyarrow'", specifier = ">=8.0.0" }, +] +provides-extras = ["all", "pandas", "polars", "pyarrow"] + +[package.metadata.requires-dev] +all = [ + { name = "fast-forward-football", marker = "python_full_version == '3.11.*'", specifier = ">=0.1.1" }, + { name = "huggingface-hub", specifier = ">=1.6.0" }, + { name = "ipykernel" }, + { name = "kloppy", specifier = ">=3.15.0" }, + { name = "matplotlib", specifier = ">=3.5.3" }, + { name = "maturin", specifier = ">=1.7.0,<2.0" }, + { name = "mplsoccer", specifier = ">=0.1.1" }, + { name = "mypy", specifier = ">=1.11.2,<2" }, + { name = "pandas", specifier = ">=1.4.4" }, + { name = "pandas-stubs" }, + { name = "pdoc" }, + { name = "polars", specifier = ">=0.16.14" }, + { name = "pre-commit", specifier = ">=4.3.0" }, + { name = "pyarrow", specifier = ">=8.0.0" }, + { name = "pytest", specifier = ">=7.1.3" }, + { name = "pytest-benchmark", specifier = ">=4.0.0,<6" }, + { name = "ruff", specifier = ">=0.12,<0.15" }, +] +benchmarks = [ + { name = "fast-forward-football", marker = "python_full_version == '3.11.*'", specifier = ">=0.1.1" }, + { name = "kloppy", specifier = ">=3.15.0" }, + { name = "maturin", specifier = ">=1.7.0,<2.0" }, + { name = "pandas", specifier = ">=1.4.4" }, + { name = "polars", specifier = ">=0.16.14" }, + { name = "pre-commit", specifier = ">=4.3.0" }, + { name = "pyarrow", specifier = ">=8.0.0" }, + { name = "pytest", specifier = ">=7.1.3" }, + { name = "pytest-benchmark", specifier = ">=4.0.0,<6" }, +] +dev = [ + { name = "maturin", specifier = ">=1.7.0,<2.0" }, + { name = "pandas", specifier = ">=1.4.4" }, + { name = "polars", specifier = ">=0.16.14" }, + { name = "pre-commit", specifier = ">=4.3.0" }, + { name = "pyarrow", specifier = ">=8.0.0" }, +] +docs = [ + { name = "maturin", specifier = ">=1.7.0,<2.0" }, + { name = "pandas", specifier = ">=1.4.4" }, + { name = "pdoc" }, + { name = "polars", specifier = ">=0.16.14" }, + { name = "pre-commit", specifier = ">=4.3.0" }, + { name = "pyarrow", specifier = ">=8.0.0" }, +] +linting = [ + { name = "maturin", specifier = ">=1.7.0,<2.0" }, + { name = "mypy", specifier = ">=1.11.2,<2" }, + { name = "pandas", specifier = ">=1.4.4" }, + { name = "pandas-stubs" }, + { name = "polars", specifier = ">=0.16.14" }, + { name = "pre-commit", specifier = ">=4.3.0" }, + { name = "pyarrow", specifier = ">=8.0.0" }, + { name = "ruff", specifier = ">=0.12,<0.15" }, +] +nb = [ + { name = "huggingface-hub", specifier = ">=1.6.0" }, + { name = "ipykernel" }, + { name = "matplotlib", specifier = ">=3.5.3" }, + { name = "mplsoccer", specifier = ">=0.1.1" }, +] +testing = [ + { name = "maturin", specifier = ">=1.7.0,<2.0" }, + { name = "pandas", specifier = ">=1.4.4" }, + { name = "polars", specifier = ">=0.16.14" }, + { name = "pre-commit", specifier = ">=4.3.0" }, + { name = "pyarrow", specifier = ">=8.0.0" }, + { name = "pytest", specifier = ">=7.1.3" }, + { name = "pytest-benchmark", specifier = ">=4.0.0,<6" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "typer" +version = "0.23.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "annotated-doc", marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "rich", marker = "python_full_version < '3.10'" }, + { name = "shellingham", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/ae/93d16574e66dfe4c2284ffdaca4b0320ade32858cb2cc586c8dd79f127c5/typer-0.23.2.tar.gz", hash = "sha256:a99706a08e54f1aef8bb6a8611503808188a4092808e86addff1828a208af0de", size = 120162, upload-time = "2026-02-16T18:52:40.354Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2c/dee705c427875402200fe779eb8a3c00ccb349471172c41178336e9599cc/typer-0.23.2-py3-none-any.whl", hash = "sha256:e9c8dc380f82450b3c851a9b9d5a0edf95d1d6456ae70c517d8b06a50c7a9978", size = 56834, upload-time = "2026-02-16T18:52:39.308Z" }, +] + +[[package]] +name = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "annotated-doc", marker = "python_full_version >= '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "rich", marker = "python_full_version >= '3.10'" }, + { name = "shellingham", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, +] + +[[package]] +name = "types-pytz" +version = "2025.2.0.20251108" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/40/ff/c047ddc68c803b46470a357454ef76f4acd8c1088f5cc4891cdd909bfcf6/types_pytz-2025.2.0.20251108.tar.gz", hash = "sha256:fca87917836ae843f07129567b74c1929f1870610681b4c92cb86a3df5817bdb", size = 10961, upload-time = "2025-11-08T02:55:57.001Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c1/56ef16bf5dcd255155cc736d276efa6ae0a5c26fd685e28f0412a4013c01/types_pytz-2025.2.0.20251108-py3-none-any.whl", hash = "sha256:0f1c9792cab4eb0e46c52f8845c8f77cf1e313cb3d68bf826aa867fe4717d91c", size = 10116, upload-time = "2025-11-08T02:55:56.194Z" }, +] + +[[package]] +name = "types-pytz" +version = "2026.1.1.20260304" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/56/2f12a15ea8c5615c8fb896c4fbbb527ab1c0f776ed5860c6fc9ec26ea2c7/types_pytz-2026.1.1.20260304.tar.gz", hash = "sha256:0c3542d8e9b0160b424233440c52b83d6f58cae4b85333d54e4f961cf013e117", size = 11198, upload-time = "2026-03-04T03:57:24.445Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/b8/e77c355f179dc89d44e7ca6dbf7a46e650806df1d356a5462e5829fccea5/types_pytz-2026.1.1.20260304-py3-none-any.whl", hash = "sha256:175332c1cf7bd6b1cc56b877f70bf02def1a3f75e5adcc05385ce2c3c70e6500", size = 10126, upload-time = "2026-03-04T03:57:23.481Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "filelock", version = "3.25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "platformdirs", version = "4.9.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "python-discovery" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/c9/18d4b36606d6091844daa3bd93cf7dc78e6f5da21d9f21d06c221104b684/virtualenv-21.1.0.tar.gz", hash = "sha256:1990a0188c8f16b6b9cf65c9183049007375b26aad415514d377ccacf1e4fb44", size = 5840471, upload-time = "2026-02-27T08:49:29.702Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/55/896b06bf93a49bec0f4ae2a6f1ed12bd05c8860744ac3a70eda041064e4d/virtualenv-21.1.0-py3-none-any.whl", hash = "sha256:164f5e14c5587d170cf98e60378eb91ea35bf037be313811905d3a24ea33cc07", size = 5825072, upload-time = "2026-02-27T08:49:27.516Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "idna", marker = "python_full_version < '3.10'" }, + { name = "multidict", marker = "python_full_version < '3.10'" }, + { name = "propcache", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/43/a2204825342f37c337f5edb6637040fa14e365b2fcc2346960201d457579/yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e", size = 140517, upload-time = "2025-10-06T14:08:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/674f3e6f02266428c56f704cd2501c22f78e8b2eeb23f153117cc86fb28a/yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f", size = 93495, upload-time = "2025-10-06T14:08:46.2Z" }, + { url = "https://files.pythonhosted.org/packages/b8/12/5b274d8a0f30c07b91b2f02cba69152600b47830fcfb465c108880fcee9c/yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf", size = 94400, upload-time = "2025-10-06T14:08:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7f/df1b6949b1fa1aa9ff6de6e2631876ad4b73c4437822026e85d8acb56bb1/yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a", size = 347545, upload-time = "2025-10-06T14:08:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/84/09/f92ed93bd6cd77872ab6c3462df45ca45cd058d8f1d0c9b4f54c1704429f/yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c", size = 319598, upload-time = "2025-10-06T14:08:51.215Z" }, + { url = "https://files.pythonhosted.org/packages/c3/97/ac3f3feae7d522cf7ccec3d340bb0b2b61c56cb9767923df62a135092c6b/yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147", size = 363893, upload-time = "2025-10-06T14:08:53.144Z" }, + { url = "https://files.pythonhosted.org/packages/06/49/f3219097403b9c84a4d079b1d7bda62dd9b86d0d6e4428c02d46ab2c77fc/yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb", size = 371240, upload-time = "2025-10-06T14:08:55.036Z" }, + { url = "https://files.pythonhosted.org/packages/35/9f/06b765d45c0e44e8ecf0fe15c9eacbbde342bb5b7561c46944f107bfb6c3/yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6", size = 346965, upload-time = "2025-10-06T14:08:56.722Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/599e7cea8d0fcb1694323b0db0dda317fa3162f7b90166faddecf532166f/yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0", size = 342026, upload-time = "2025-10-06T14:08:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/95/6f/9dfd12c8bc90fea9eab39832ee32ea48f8e53d1256252a77b710c065c89f/yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda", size = 335637, upload-time = "2025-10-06T14:09:00.506Z" }, + { url = "https://files.pythonhosted.org/packages/57/2e/34c5b4eb9b07e16e873db5b182c71e5f06f9b5af388cdaa97736d79dd9a6/yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", size = 359082, upload-time = "2025-10-06T14:09:01.936Z" }, + { url = "https://files.pythonhosted.org/packages/31/71/fa7e10fb772d273aa1f096ecb8ab8594117822f683bab7d2c5a89914c92a/yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", size = 357811, upload-time = "2025-10-06T14:09:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/26/da/11374c04e8e1184a6a03cf9c8f5688d3e5cec83ed6f31ad3481b3207f709/yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", size = 351223, upload-time = "2025-10-06T14:09:05.401Z" }, + { url = "https://files.pythonhosted.org/packages/82/8f/e2d01f161b0c034a30410e375e191a5d27608c1f8693bab1a08b089ca096/yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea", size = 82118, upload-time = "2025-10-06T14:09:11.148Z" }, + { url = "https://files.pythonhosted.org/packages/62/46/94c76196642dbeae634c7a61ba3da88cd77bed875bf6e4a8bed037505aa6/yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca", size = 86852, upload-time = "2025-10-06T14:09:12.958Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/7df4f179d3b1a6dcb9a4bd2ffbc67642746fcafdb62580e66876ce83fff4/yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b", size = 82012, upload-time = "2025-10-06T14:09:14.664Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, + { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, + { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, + { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, + { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, + { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, + { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, + { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, + { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, + { url = "https://files.pythonhosted.org/packages/94/fd/6480106702a79bcceda5fd9c63cb19a04a6506bd5ce7fd8d9b63742f0021/yarl-1.22.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748", size = 141301, upload-time = "2025-10-06T14:12:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/42/e1/6d95d21b17a93e793e4ec420a925fe1f6a9342338ca7a563ed21129c0990/yarl-1.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859", size = 93864, upload-time = "2025-10-06T14:12:21.05Z" }, + { url = "https://files.pythonhosted.org/packages/32/58/b8055273c203968e89808413ea4c984988b6649baabf10f4522e67c22d2f/yarl-1.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9", size = 94706, upload-time = "2025-10-06T14:12:23.287Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/d7bfbc28a88c2895ecd0da6a874def0c147de78afc52c773c28e1aa233a3/yarl-1.22.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054", size = 347100, upload-time = "2025-10-06T14:12:28.527Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e8/37a1e7b99721c0564b1fc7b0a4d1f595ef6fb8060d82ca61775b644185f7/yarl-1.22.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b", size = 318902, upload-time = "2025-10-06T14:12:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ef/34724449d7ef2db4f22df644f2dac0b8a275d20f585e526937b3ae47b02d/yarl-1.22.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60", size = 363302, upload-time = "2025-10-06T14:12:32.295Z" }, + { url = "https://files.pythonhosted.org/packages/8a/04/88a39a5dad39889f192cce8d66cc4c58dbeca983e83f9b6bf23822a7ed91/yarl-1.22.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890", size = 370816, upload-time = "2025-10-06T14:12:34.01Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1f/5e895e547129413f56c76be2c3ce4b96c797d2d0ff3e16a817d9269b12e6/yarl-1.22.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba", size = 346465, upload-time = "2025-10-06T14:12:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/11/13/a750e9fd6f9cc9ed3a52a70fe58ffe505322f0efe0d48e1fd9ffe53281f5/yarl-1.22.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca", size = 341506, upload-time = "2025-10-06T14:12:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/3c/67/bb6024de76e7186611ebe626aec5b71a2d2ecf9453e795f2dbd80614784c/yarl-1.22.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba", size = 335030, upload-time = "2025-10-06T14:12:39.775Z" }, + { url = "https://files.pythonhosted.org/packages/a2/be/50b38447fd94a7992996a62b8b463d0579323fcfc08c61bdba949eef8a5d/yarl-1.22.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b", size = 358560, upload-time = "2025-10-06T14:12:41.547Z" }, + { url = "https://files.pythonhosted.org/packages/e2/89/c020b6f547578c4e3dbb6335bf918f26e2f34ad0d1e515d72fd33ac0c635/yarl-1.22.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e", size = 357290, upload-time = "2025-10-06T14:12:43.861Z" }, + { url = "https://files.pythonhosted.org/packages/8c/52/c49a619ee35a402fa3a7019a4fa8d26878fec0d1243f6968bbf516789578/yarl-1.22.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8", size = 350700, upload-time = "2025-10-06T14:12:46.868Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c9/f5042d87777bf6968435f04a2bbb15466b2f142e6e47fa4f34d1a3f32f0c/yarl-1.22.0-cp39-cp39-win32.whl", hash = "sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b", size = 82323, upload-time = "2025-10-06T14:12:48.633Z" }, + { url = "https://files.pythonhosted.org/packages/fd/58/d00f7cad9eba20c4eefac2682f34661d1d1b3a942fc0092eb60e78cfb733/yarl-1.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed", size = 87145, upload-time = "2025-10-06T14:12:50.241Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a3/70904f365080780d38b919edd42d224b8c4ce224a86950d2eaa2a24366ad/yarl-1.22.0-cp39-cp39-win_arm64.whl", hash = "sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2", size = 82173, upload-time = "2025-10-06T14:12:51.869Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +] + +[[package]] +name = "yarl" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "idna", marker = "python_full_version >= '3.10'" }, + { name = "multidict", marker = "python_full_version >= '3.10'" }, + { name = "propcache", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0d/9cc638702f6fc3c7a3685bcc8cf2a9ed7d6206e932a49f5242658047ef51/yarl-1.23.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107", size = 123764, upload-time = "2026-03-01T22:04:09.7Z" }, + { url = "https://files.pythonhosted.org/packages/7a/35/5a553687c5793df5429cd1db45909d4f3af7eee90014888c208d086a44f0/yarl-1.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d", size = 86282, upload-time = "2026-03-01T22:04:11.892Z" }, + { url = "https://files.pythonhosted.org/packages/68/2e/c5a2234238f8ce37a8312b52801ee74117f576b1539eec8404a480434acc/yarl-1.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05", size = 86053, upload-time = "2026-03-01T22:04:13.292Z" }, + { url = "https://files.pythonhosted.org/packages/74/3f/bbd8ff36fb038622797ffbaf7db314918bb4d76f1cc8a4f9ca7a55fe5195/yarl-1.23.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d", size = 99395, upload-time = "2026-03-01T22:04:15.133Z" }, + { url = "https://files.pythonhosted.org/packages/77/04/9516bc4e269d2a3ec9c6779fcdeac51ce5b3a9b0156f06ac7152e5bba864/yarl-1.23.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748", size = 92143, upload-time = "2026-03-01T22:04:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/c7/63/88802d1f6b1cb1fc67d67a58cd0cf8a1790de4ce7946e434240f1d60ab4a/yarl-1.23.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764", size = 107643, upload-time = "2026-03-01T22:04:18.519Z" }, + { url = "https://files.pythonhosted.org/packages/8e/db/4f9b838f4d8bdd6f0f385aed8bbf21c71ed11a0b9983305c302cbd557815/yarl-1.23.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007", size = 108700, upload-time = "2026-03-01T22:04:20.373Z" }, + { url = "https://files.pythonhosted.org/packages/50/12/95a1d33f04a79c402664070d43b8b9f72dc18914e135b345b611b0b1f8cc/yarl-1.23.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4", size = 102769, upload-time = "2026-03-01T22:04:23.055Z" }, + { url = "https://files.pythonhosted.org/packages/86/65/91a0285f51321369fd1a8308aa19207520c5f0587772cfc2e03fc2467e90/yarl-1.23.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26", size = 101114, upload-time = "2026-03-01T22:04:25.031Z" }, + { url = "https://files.pythonhosted.org/packages/58/80/c7c8244fc3e5bc483dc71a09560f43b619fab29301a0f0a8f936e42865c7/yarl-1.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769", size = 98883, upload-time = "2026-03-01T22:04:27.281Z" }, + { url = "https://files.pythonhosted.org/packages/86/e7/71ca9cc9ca79c0b7d491216177d1aed559d632947b8ffb0ee60f7d8b23e3/yarl-1.23.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716", size = 94172, upload-time = "2026-03-01T22:04:28.554Z" }, + { url = "https://files.pythonhosted.org/packages/6a/3f/6c6c8a0fe29c26fb2db2e8d32195bb84ec1bfb8f1d32e7f73b787fcf349b/yarl-1.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993", size = 107010, upload-time = "2026-03-01T22:04:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/56/38/12730c05e5ad40a76374d440ed8b0899729a96c250516d91c620a6e38fc2/yarl-1.23.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0", size = 100285, upload-time = "2026-03-01T22:04:31.752Z" }, + { url = "https://files.pythonhosted.org/packages/34/92/6a7be9239f2347234e027284e7a5f74b1140cc86575e7b469d13fba1ebfe/yarl-1.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750", size = 108230, upload-time = "2026-03-01T22:04:33.844Z" }, + { url = "https://files.pythonhosted.org/packages/5e/81/4aebccfa9376bd98b9d8bfad20621a57d3e8cfc5b8631c1fa5f62cdd03f4/yarl-1.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6", size = 103008, upload-time = "2026-03-01T22:04:35.856Z" }, + { url = "https://files.pythonhosted.org/packages/38/0f/0b4e3edcec794a86b853b0c6396c0a888d72dfce19b2d88c02ac289fb6c1/yarl-1.23.0-cp310-cp310-win32.whl", hash = "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d", size = 83073, upload-time = "2026-03-01T22:04:38.268Z" }, + { url = "https://files.pythonhosted.org/packages/a0/71/ad95c33da18897e4c636528bbc24a1dd23fe16797de8bc4ec667b8db0ba4/yarl-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb", size = 87328, upload-time = "2026-03-01T22:04:39.558Z" }, + { url = "https://files.pythonhosted.org/packages/e2/14/dfa369523c79bccf9c9c746b0a63eb31f65db9418ac01275f7950962e504/yarl-1.23.0-cp310-cp310-win_arm64.whl", hash = "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220", size = 82463, upload-time = "2026-03-01T22:04:41.454Z" }, + { url = "https://files.pythonhosted.org/packages/a2/aa/60da938b8f0997ba3a911263c40d82b6f645a67902a490b46f3355e10fae/yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", size = 123641, upload-time = "2026-03-01T22:04:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", size = 86248, upload-time = "2026-03-01T22:04:44.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", size = 85988, upload-time = "2026-03-01T22:04:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6c/4a90d59c572e46b270ca132aca66954f1175abd691f74c1ef4c6711828e2/yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", size = 100566, upload-time = "2026-03-01T22:04:47.639Z" }, + { url = "https://files.pythonhosted.org/packages/49/fb/c438fb5108047e629f6282a371e6e91cf3f97ee087c4fb748a1f32ceef55/yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", size = 92079, upload-time = "2026-03-01T22:04:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/d9/13/d269aa1aed3e4f50a5a103f96327210cc5fa5dd2d50882778f13c7a14606/yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", size = 108741, upload-time = "2026-03-01T22:04:50.838Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/115b16f22c37ea4437d323e472945bea97301c8ec6089868fa560abab590/yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", size = 108099, upload-time = "2026-03-01T22:04:52.499Z" }, + { url = "https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", size = 102678, upload-time = "2026-03-01T22:04:55.176Z" }, + { url = "https://files.pythonhosted.org/packages/85/59/cd98e556fbb2bf8fab29c1a722f67ad45c5f3447cac798ab85620d1e70af/yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", size = 100803, upload-time = "2026-03-01T22:04:56.588Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/b39770b56d4a9f0bb5f77e2f1763cd2d75cc2f6c0131e3b4c360348fcd65/yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", size = 100163, upload-time = "2026-03-01T22:04:58.492Z" }, + { url = "https://files.pythonhosted.org/packages/e7/64/6980f99ab00e1f0ff67cb84766c93d595b067eed07439cfccfc8fb28c1a6/yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", size = 93859, upload-time = "2026-03-01T22:05:00.268Z" }, + { url = "https://files.pythonhosted.org/packages/38/69/912e6c5e146793e5d4b5fe39ff5b00f4d22463dfd5a162bec565ac757673/yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", size = 108202, upload-time = "2026-03-01T22:05:02.273Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/35ca6767524687ad64e5f5c31ad54bc76d585585a9fcb40f649e7e82ffed/yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", size = 99866, upload-time = "2026-03-01T22:05:03.597Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1c/1a3387ee6d73589f6f2a220ae06f2984f6c20b40c734989b0a44f5987308/yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", size = 107852, upload-time = "2026-03-01T22:05:04.986Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/35c0750fcd5a3f781058bfd954515dd4b1eab45e218cbb85cf11132215f1/yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", size = 102919, upload-time = "2026-03-01T22:05:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/9a1979aec4a81896d597bcb2177827f2dbee3f5b7cc48b2d0dadb644b41d/yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", size = 82602, upload-time = "2026-03-01T22:05:08.444Z" }, + { url = "https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", size = 87461, upload-time = "2026-03-01T22:05:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/93/95/07e3553fe6f113e6864a20bdc53a78113cda3b9ced8784ee52a52c9f80d8/yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", size = 82336, upload-time = "2026-03-01T22:05:11.554Z" }, + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] From cd3168737ceb31716cd8b1e8cd104c5b74782920 Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Fri, 3 Apr 2026 15:19:55 +0200 Subject: [PATCH 02/21] refactor: clean up old tests folder --- tests/test_builder.rs | 118 ------------------------------------------ 1 file changed, 118 deletions(-) delete mode 100644 tests/test_builder.rs diff --git a/tests/test_builder.rs b/tests/test_builder.rs deleted file mode 100644 index 22e6d92..0000000 --- a/tests/test_builder.rs +++ /dev/null @@ -1,118 +0,0 @@ -use tracking_data_container::TrackingDataBuilder; -use quick_xml::reader::Reader; -use quick_xml::events::Event; -use rayon::prelude::*; -use std::collections::HashMap; - -#[test] -fn test_parse_sportec_xml() { - let path = "tests/files/sportec_positional.xml"; - let xml_content = std::fs::read_to_string(path).expect("XML file should exist"); - - // 1. Fast scan to find FrameSet chunks - let mut reader = Reader::from_str(&xml_content); - let mut chunks = Vec::new(); - let mut buf = Vec::new(); - - loop { - match reader.read_event_into(&mut buf) { - Ok(Event::Start(ref e)) if e.name().as_ref() == b"FrameSet" => { - // We need the attributes - let mut team_id = String::new(); - let mut person_id = String::new(); - for attr in e.attributes() { - let a = attr.unwrap(); - if a.key.as_ref() == b"TeamId" { - team_id = a.unescape_value().unwrap().into_owned(); - } else if a.key.as_ref() == b"PersonId" { - person_id = a.unescape_value().unwrap().into_owned(); - } - } - - // Collect all Frame events until End tag - let mut frames = Vec::new(); - let mut frame_buf = Vec::new(); - loop { - match reader.read_event_into(&mut frame_buf) { - Ok(Event::Empty(ref fe)) if fe.name().as_ref() == b"Frame" => { - let mut f_data = HashMap::new(); - for attr in fe.attributes() { - let a = attr.unwrap(); - let key = std::str::from_utf8(a.key.as_ref()).unwrap().to_string(); - let val = a.unescape_value().unwrap().into_owned(); - f_data.insert(key, val); - } - frames.push(f_data); - } - Ok(Event::End(ref ee)) if ee.name().as_ref() == b"FrameSet" => break, - _ => {} - } - frame_buf.clear(); - } - chunks.push((team_id, person_id, frames)); - } - Ok(Event::Eof) => break, - _ => {} - } - buf.clear(); - } - - // 2. Parallel processing of chunks using the builder - let final_builder = chunks - .into_par_iter() - .map(|(team_id, person_id, frames)| { - let mut builder = TrackingDataBuilder::new(); - let prefix = if team_id == "BALL" { - "ball".to_string() - } else { - format!("teams/{}/players/{}", team_id, person_id) - }; - - for f in frames { - let n: i64 = f.get("N").unwrap().parse().unwrap(); - - if let Some(x) = f.get("X") { - builder.insert_float(n, &format!("{}/x", prefix), x.parse().unwrap()); - } - if let Some(y) = f.get("Y") { - builder.insert_float(n, &format!("{}/y", prefix), y.parse().unwrap()); - } - if let Some(z) = f.get("Z") { - builder.insert_float(n, &format!("{}/z", prefix), z.parse().unwrap()); - } - if let Some(s) = f.get("S") { - builder.insert_float(n, &format!("{}/vel", prefix), s.parse().unwrap()); - } - if let Some(d) = f.get("D") { - builder.insert_float(n, &format!("{}/dist", prefix), d.parse().unwrap()); - } - if let Some(a) = f.get("A") { - builder.insert_float(n, &format!("{}/acc", prefix), a.parse().unwrap()); - } - if let Some(m) = f.get("M") { - builder.insert_int(n, "period", m.parse().unwrap()); - } - if let Some(bp) = f.get("BallPossession") { - builder.insert_string(n, "ball_poss_team_id", bp.clone()); - } - if let Some(bs) = f.get("BallStatus") { - builder.insert_int(n, "ball_status", bs.parse().unwrap()); - } - } - builder - }) - .reduce(TrackingDataBuilder::new, |mut a, b| { - a.merge(b); - a - }); - - // 3. Build and verify - let container = final_builder.build().unwrap(); - assert!(container.num_rows() > 0); - - // Check ball/x existence - assert!(container.schema.fields.iter().any(|f| f.name() == "ball/x")); - - // Check some player existence (from the XML personId) - assert!(container.schema.fields.iter().any(|f| f.name().contains("DFL-OBJ-002G3I"))); -} From 892b20fe77a3ffa85f7ce6454ca634524c931875 Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Fri, 3 Apr 2026 22:19:58 +0200 Subject: [PATCH 03/21] feat: support skeletal data --- Cargo.lock | 13 + crates/tdc-core/src/datacontainer/core.rs | 9 + crates/tdc-core/src/datacontainer/lazy.rs | 10 + crates/tdc-core/src/schema/attribute.rs | 35 +- crates/tdc-core/src/schema/field.rs | 33 +- crates/tdc-core/src/selector/expr.rs | 24 + crates/tdc-parsers/Cargo.toml | 5 +- crates/tdc-parsers/src/hawkeye.rs | 459 + crates/tdc-parsers/src/lib.rs | 27 + crates/tdc-parsers/tests/test_hawkeye.rs | 257 + crates/tdc-python/src/container.rs | 10 + crates/tdc-python/src/selector.rs | 5 + examples/06-skeletal.ipynb | 226969 +++++++++++++++ python/tests/benchmarks/test_hawkeye.py | 108 + .../tests/benchmarks/test_lazy_filtering.py | 2 +- python/tests/benchmarks/test_sportec.py | 4 +- python/tests/benchmarks/test_statsperform.py | 4 +- python/tests/test_select.py | 19 + python/tracking_data_container/__init__.py | 3 +- .../_tracking_data_container.pyi | 13 +- python/tracking_data_container/container.py | 40 + python/tracking_data_container/io.py | 37 + python/tracking_data_container/selector.py | 4 + 23 files changed, 228074 insertions(+), 16 deletions(-) create mode 100644 crates/tdc-parsers/src/hawkeye.rs create mode 100644 crates/tdc-parsers/tests/test_hawkeye.rs create mode 100644 examples/06-skeletal.ipynb create mode 100644 python/tests/benchmarks/test_hawkeye.py diff --git a/Cargo.lock b/Cargo.lock index 545fe1d..c74a2fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,7 @@ dependencies = [ "const-random", "getrandom 0.3.4", "once_cell", + "serde", "version_check", "zerocopy", ] @@ -1837,6 +1838,15 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -2782,9 +2792,12 @@ dependencies = [ "chrono", "criterion", "fast-float", + "memmap2", "pyo3", "quick-xml", "rayon", + "serde", + "serde_json", "tdc-core", "tempfile", ] diff --git a/crates/tdc-core/src/datacontainer/core.rs b/crates/tdc-core/src/datacontainer/core.rs index 46d1d08..90cb2c1 100644 --- a/crates/tdc-core/src/datacontainer/core.rs +++ b/crates/tdc-core/src/datacontainer/core.rs @@ -36,6 +36,15 @@ impl TrackingDataContainer { self.data.is_empty() } + pub fn slice(&self, offset: usize, length: usize) -> Self { + Self { + data: self.data.slice(offset, length), + schema: self.schema.clone(), + coordinate_system: self.coordinate_system.clone(), + orientation: self.orientation, + } + } + pub fn validate(&self) -> TrackingDataContainerResult<()> { self.schema.validate() } diff --git a/crates/tdc-core/src/datacontainer/lazy.rs b/crates/tdc-core/src/datacontainer/lazy.rs index 297fa02..62889d5 100644 --- a/crates/tdc-core/src/datacontainer/lazy.rs +++ b/crates/tdc-core/src/datacontainer/lazy.rs @@ -54,6 +54,16 @@ impl LazyTrackingDataContainer { self } + pub fn slice(&self, offset: usize, length: usize) -> TrackingDataContainerResult { + let df = self.df.clone().limit(offset, Some(length))?; + Ok(Self { + df, + schema: self.schema.clone(), + coordinate_system: self.coordinate_system.clone(), + orientation: self.orientation, + }) + } + pub fn filter( &self, predicate: datafusion::prelude::Expr, diff --git a/crates/tdc-core/src/schema/attribute.rs b/crates/tdc-core/src/schema/attribute.rs index c407a44..f3fa88d 100644 --- a/crates/tdc-core/src/schema/attribute.rs +++ b/crates/tdc-core/src/schema/attribute.rs @@ -4,6 +4,7 @@ //! in sports tracking, such as spatial coordinates, velocity, and match metadata. use arrow_schema::{DataType, TimeUnit}; +use std::sync::Arc; use strum_macros::{AsRefStr, Display, EnumString}; /// Common trait for all schema attributes. @@ -48,6 +49,20 @@ pub enum TrackedEntityAttribute { /// Cumulative minutes played by the entity. MinutesPlayed, + // Landmark / Skeletal attributes + /// Unique identifier for a landmark. + LandmarkIndex(String), + /// Relative x coordinate of landmark. + LandmarkX(String), + /// Relative y coordinate of landmark. + LandmarkY(String), + /// Relative z coordinate of landmark. + LandmarkZ(String), + /// List of children indexes associated with keypoint. + LandmarkChildren(String), + /// If landmark is detected (true) or inferred (false). + LandmarkIsVisible(String), + // Team specific /// Official name of the team or entity. Name, @@ -64,6 +79,12 @@ impl Attribute for TrackedEntityAttribute { fn name(&self) -> &str { match self { Self::Other(s) => s, + Self::LandmarkIndex(_) => "index", + Self::LandmarkX(_) => "x", + Self::LandmarkY(_) => "y", + Self::LandmarkZ(_) => "z", + Self::LandmarkChildren(_) => "children", + Self::LandmarkIsVisible(_) => "is_visible", _ => self.as_ref(), } } @@ -72,15 +93,21 @@ impl Attribute for TrackedEntityAttribute { fn default_arrow_type(&self) -> DataType { use TrackedEntityAttribute::*; match self { - Id | Other(_) | Name | JerseyColour | Formation => DataType::Utf8, - IsVisible => DataType::Boolean, - X | Y | Z | Dist | Vel | Acc | Lat | Long | MinutesPlayed => DataType::Float32, + Id | Other(_) | Name | JerseyColour | Formation | LandmarkIndex(_) => DataType::Utf8, + IsVisible | LandmarkIsVisible(_) => DataType::Boolean, + X | Y | Z | Dist | Vel | Acc | Lat | Long | MinutesPlayed | LandmarkX(_) + | LandmarkY(_) | LandmarkZ(_) => DataType::Float32, + LandmarkChildren(_) => DataType::List(Arc::new(arrow_schema::Field::new( + "item", + DataType::Int32, + true, + ))), } } fn is_low_cardinality(&self) -> bool { use TrackedEntityAttribute::*; - matches!(self, Formation | JerseyColour) + matches!(self, Formation | JerseyColour | LandmarkIndex(_)) } } diff --git a/crates/tdc-core/src/schema/field.rs b/crates/tdc-core/src/schema/field.rs index 2cee717..e608633 100644 --- a/crates/tdc-core/src/schema/field.rs +++ b/crates/tdc-core/src/schema/field.rs @@ -45,9 +45,17 @@ impl DataField { let attr_name = attribute.name(); match entity { TrackedEntity::Ball => format!("ball/{attr_name}"), - TrackedEntity::Player { team_id, player_id } => { - format!("teams/{team_id}/players/{player_id}/{attr_name}") - } + TrackedEntity::Player { team_id, player_id } => match attribute { + TrackedEntityAttribute::LandmarkIndex(j) + | TrackedEntityAttribute::LandmarkX(j) + | TrackedEntityAttribute::LandmarkY(j) + | TrackedEntityAttribute::LandmarkZ(j) + | TrackedEntityAttribute::LandmarkChildren(j) + | TrackedEntityAttribute::LandmarkIsVisible(j) => { + format!("teams/{team_id}/players/{player_id}/landmarks/{j}/{attr_name}") + } + _ => format!("teams/{team_id}/players/{player_id}/{attr_name}"), + }, TrackedEntity::Referee { referee_id } => { format!("referees/{referee_id}/{attr_name}") } @@ -66,6 +74,25 @@ impl DataField { let parts: Vec<&str> = path.split('/').collect(); match parts.as_slice() { + ["teams", tid, "players", pid, "landmarks", j, ..] => { + let attr_str = parts[6..].join("/"); + let attribute = match attr_str.as_str() { + "index" => TrackedEntityAttribute::LandmarkIndex(j.to_string()), + "x" => TrackedEntityAttribute::LandmarkX(j.to_string()), + "y" => TrackedEntityAttribute::LandmarkY(j.to_string()), + "z" => TrackedEntityAttribute::LandmarkZ(j.to_string()), + "children" => TrackedEntityAttribute::LandmarkChildren(j.to_string()), + "is_visible" => TrackedEntityAttribute::LandmarkIsVisible(j.to_string()), + _ => Self::parse_attr(&attr_str), + }; + DataField::TrackedEntity { + entity: TrackedEntity::Player { + team_id: tid.to_string(), + player_id: pid.to_string(), + }, + attribute, + } + } ["teams", tid, "players", pid, ..] => { let attr_str = parts[4..].join("/"); DataField::TrackedEntity { diff --git a/crates/tdc-core/src/selector/expr.rs b/crates/tdc-core/src/selector/expr.rs index 742fe52..b5b0ab0 100644 --- a/crates/tdc-core/src/selector/expr.rs +++ b/crates/tdc-core/src/selector/expr.rs @@ -10,6 +10,10 @@ pub enum Expr { Players { team_id: Option, }, + Player { + team_id: Option, + player_id: String, + }, Team { team_id: String, }, @@ -56,6 +60,26 @@ pub fn expand_expr(container: &impl crate::selector::expr::HasSchema, expr: &Exp }) .map(|f| f.name()) .collect(), + Expr::Player { team_id, player_id } => schema + .fields + .iter() + .filter(|f| { + if let DataField::TrackedEntity { + entity: + TrackedEntity::Player { + team_id: tid, + player_id: pid, + }, + .. + } = f + { + (team_id.is_none() || team_id.as_ref() == Some(tid)) && player_id == pid + } else { + false + } + }) + .map(|f| f.name()) + .collect(), Expr::Team { team_id } => schema .fields .iter() diff --git a/crates/tdc-parsers/Cargo.toml b/crates/tdc-parsers/Cargo.toml index d445063..1d02b58 100644 --- a/crates/tdc-parsers/Cargo.toml +++ b/crates/tdc-parsers/Cargo.toml @@ -14,8 +14,11 @@ tdc-core = { path = "../tdc-core" } arrow-array = { version = "54" } arrow-schema = { version = "54" } quick-xml = "0.37" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" rayon = "1.10" -ahash = "0.8" +memmap2 = "0.9" +ahash = { version = "0.8", features = ["serde"] } fast-float = "0.2" chrono = { version = "^0.4.42", default-features = false } pyo3 = { version = "0.23", features = ["abi3-py310"], optional = true } diff --git a/crates/tdc-parsers/src/hawkeye.rs b/crates/tdc-parsers/src/hawkeye.rs new file mode 100644 index 0000000..53d6b38 --- /dev/null +++ b/crates/tdc-parsers/src/hawkeye.rs @@ -0,0 +1,459 @@ +use ahash::AHashMap; +use arrow_schema::DataType; +use memmap2::Mmap; +use rayon::prelude::*; +use serde::Deserialize; +use std::fs::{self, File}; +use tdc_core::datacontainer::{TrackingDataBuilder, TrackingDataContainer}; +use tdc_core::schema::{CoordinateSystem, Direction, LengthUnit, Origin}; + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(bound(deserialize = "'de: 'a"))] +struct HawkEyeBallData<'a> { + details: BallDetails<'a>, + samples: BallSamples, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(bound(deserialize = "'de: 'a"))] +struct BallDetails<'a> { + teams: Vec>, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(bound(deserialize = "'de: 'a"))] +struct TeamInfo<'a> { + id: IdInfo<'a>, + _name: &'a str, + home: bool, +} + +#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +#[serde(rename_all = "camelCase")] +#[serde(bound(deserialize = "'de: 'a"))] +struct IdInfo<'a> { + he_id: &'a str, + _uefa_id: Option<&'a str>, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct BallSamples { + ball: Vec, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct BallSample { + pos: [f32; 3], + _time: f32, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(bound(deserialize = "'de: 'a"))] +struct HawkEyeCentroidData<'a> { + details: CentroidDetails<'a>, + samples: CentroidSamples<'a>, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(bound(deserialize = "'de: 'a"))] +struct CentroidDetails<'a> { + players: Vec>, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(bound(deserialize = "'de: 'a"))] +struct PlayerInfo<'a> { + id: IdInfo<'a>, + team_id: IdInfo<'a>, + jersey_number: &'a str, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(bound(deserialize = "'de: 'a"))] +struct CentroidSamples<'a> { + people: Vec>, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(bound(deserialize = "'de: 'a"))] +struct PersonSample<'a> { + person_id: IdInfo<'a>, + centroid: Vec, + role: RoleInfo<'a>, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(bound(deserialize = "'de: 'a"))] +struct RoleInfo<'a> { + _id: u32, + name: &'a str, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct CentroidSample { + pos: [f32; 2], + time: f32, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "PascalCase")] +struct HawkEyeMetaData { + stadium: StadiumMetaData, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "PascalCase")] +struct StadiumMetaData { + pitch_length: f64, + pitch_width: f64, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(bound(deserialize = "'de: 'a"))] +struct HawkEyeJointData<'a> { + samples: JointSamples<'a>, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(bound(deserialize = "'de: 'a"))] +struct JointSamples<'a> { + people: Vec>, +} + +#[derive(Deserialize, Debug)] +#[serde(untagged)] +enum JointValue { + Pos([f32; 3]), + Time(f32), +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(bound(deserialize = "'de: 'a"))] +struct PersonJointSample<'a> { + person_id: IdInfo<'a>, + joints: Vec>, +} + +pub fn load_hawkeye( + ball_paths: &[String], + centroid_paths: &[String], + joint_paths: Option<&[String]>, + meta_path: Option<&str>, +) -> Result { + if ball_paths.is_empty() { + return Err("No paths provided".to_string()); + } + if ball_paths.len() != centroid_paths.len() { + return Err("Number of ball and centroid files must match".to_string()); + } + if let Some(jp) = joint_paths + && jp.len() != ball_paths.len() + { + return Err("Number of joint files must match ball and centroid files".to_string()); + } + + // 1. Initial Metadata Pass (from first file) + let ball_file = File::open(&ball_paths[0]).map_err(|e| e.to_string())?; + let ball_mmap = unsafe { Mmap::map(&ball_file).map_err(|e| e.to_string())? }; + let ball_data: HawkEyeBallData = + serde_json::from_slice(&ball_mmap).map_err(|e| e.to_string())?; + + let centroid_file = File::open(¢roid_paths[0]).map_err(|e| e.to_string())?; + let centroid_mmap = unsafe { Mmap::map(¢roid_file).map_err(|e| e.to_string())? }; + let centroid_data: HawkEyeCentroidData = + serde_json::from_slice(¢roid_mmap).map_err(|e| e.to_string())?; + + let mut team_map = AHashMap::new(); // heId -> "home" / "away" + for team in ball_data.details.teams { + let role = if team.home { "home" } else { "away" }; + team_map.insert(team.id.he_id.to_string(), role.to_string()); + } + + let mut player_map = AHashMap::new(); // heId -> (role, jersey) + for player in centroid_data.details.players { + if let Some(role) = team_map.get(player.team_id.he_id) { + player_map.insert( + player.id.he_id.to_string(), + (role.clone(), player.jersey_number.to_string()), + ); + } + } + + // 2. Pre-pass: Find total frames and offsets per file + const FREQ: f32 = 25.0; + + let mut lengths = Vec::with_capacity(ball_paths.len()); + for p in ball_paths { + let file = File::open(p).map_err(|e| e.to_string())?; + let mmap = unsafe { Mmap::map(&file).map_err(|e| e.to_string())? }; + let b_data: HawkEyeBallData = serde_json::from_slice(&mmap).map_err(|e| e.to_string())?; + lengths.push(b_data.samples.ball.len()); + } + + let mut offsets = Vec::with_capacity(lengths.len()); + let mut current_offset = 0; + for &l in &lengths { + offsets.push(current_offset); + current_offset += l; + } + + // 3. Batched Parallel Processing of minutes to limit peak memory + let mut builders = Vec::new(); + let indices: Vec = (0..ball_paths.len()).collect(); + + for chunk in indices.chunks(8) { + let chunk_builders: Vec> = chunk + .into_par_iter() + .map(|&idx| -> Result { + let bp = &ball_paths[idx]; + let cp = ¢roid_paths[idx]; + let jp = joint_paths.map(|p| &p[idx]); + let offset = offsets[idx]; + let minute_len = lengths[idx]; + + let b_file = File::open(bp).map_err(|e| e.to_string())?; + let b_mmap = unsafe { Mmap::map(&b_file).map_err(|e| e.to_string())? }; + let b_data: HawkEyeBallData = + serde_json::from_slice(&b_mmap).map_err(|e| e.to_string())?; + + let c_file = File::open(cp).map_err(|e| e.to_string())?; + let c_mmap = unsafe { Mmap::map(&c_file).map_err(|e| e.to_string())? }; + let c_data: HawkEyeCentroidData = + serde_json::from_slice(&c_mmap).map_err(|e| e.to_string())?; + + let period_name = if bp.contains("_1_") { + "first_half" + } else if bp.contains("_2_") { + "second_half" + } else { + "other" + }; + + let mut builder = TrackingDataBuilder::new_fixed( + offset as i64, + (offset + minute_len).saturating_sub(1) as i64, + ); + let c_period = builder.get_or_create_column_id("period", DataType::Utf8); + let c_vendor = + builder.get_or_create_column_id("vendor/tracking/name", DataType::Utf8); + + let c_bx = builder.get_or_create_column_id("ball/x", DataType::Float32); + let c_by = builder.get_or_create_column_id("ball/y", DataType::Float32); + let c_bz = builder.get_or_create_column_id("ball/z", DataType::Float32); + + // Ball samples + for (i, sample) in b_data.samples.ball.iter().enumerate() { + builder.insert_string_at_idx(i, c_period, period_name); + builder.insert_string_at_idx(i, c_vendor, "hawkeye"); + builder.insert_float_at_idx(i, c_bx, sample.pos[0] as f64); + builder.insert_float_at_idx(i, c_by, sample.pos[1] as f64); + builder.insert_float_at_idx(i, c_bz, sample.pos[2] as f64); + } + + // Optimization: Cache Column IDs to avoid massive string formatting and schema lookups + let mut col_cache: AHashMap<(&str, &str, char), usize> = AHashMap::new(); + + // Player samples (centroids) + let mut current_frame = 0; + let mut last_time = -1.0; + + for person in c_data.samples.people { + let sample = match person.centroid.first() { + Some(s) => s, + None => continue, + }; + if (sample.time - last_time).abs() > (1.0 / (2.0 * FREQ)) { + if last_time != -1.0 { + current_frame += 1; + } + last_time = sample.time; + } + + if current_frame >= minute_len { + continue; + } + + if let Some((role, jersey)) = player_map.get(person.person_id.he_id) { + let pid = person.person_id.he_id; + let ix = *col_cache.entry((pid, "centroid", 'x')).or_insert_with(|| { + builder.get_or_create_column_id( + &format!("teams/{}/players/{}/x", role, jersey), + DataType::Float32, + ) + }); + let iy = *col_cache.entry((pid, "centroid", 'y')).or_insert_with(|| { + builder.get_or_create_column_id( + &format!("teams/{}/players/{}/y", role, jersey), + DataType::Float32, + ) + }); + builder.insert_float_at_idx(current_frame, ix, sample.pos[0] as f64); + builder.insert_float_at_idx(current_frame, iy, sample.pos[1] as f64); + } else if person.role.name.contains("Referee") { + let pid = person.person_id.he_id; + let ix = *col_cache.entry((pid, "centroid", 'x')).or_insert_with(|| { + builder.get_or_create_column_id( + &format!("referees/{}/x", pid), + DataType::Float32, + ) + }); + let iy = *col_cache.entry((pid, "centroid", 'y')).or_insert_with(|| { + builder.get_or_create_column_id( + &format!("referees/{}/y", pid), + DataType::Float32, + ) + }); + builder.insert_float_at_idx(current_frame, ix, sample.pos[0] as f64); + builder.insert_float_at_idx(current_frame, iy, sample.pos[1] as f64); + } + } + + // Player samples (joints) + if let Some(j_path) = jp { + let j_file = File::open(j_path).map_err(|e| e.to_string())?; + let j_mmap = unsafe { Mmap::map(&j_file).map_err(|e| e.to_string())? }; + let j_data: HawkEyeJointData = + serde_json::from_slice(&j_mmap).map_err(|e| e.to_string())?; + + let mut current_frame = 0; + let mut last_time = -1.0; + + for person in j_data.samples.people { + let joints_map = match person.joints.first() { + Some(m) => m, + None => continue, + }; + let time = match joints_map.get("time") { + Some(JointValue::Time(t)) => *t, + _ => 0.0, + }; + + if (time - last_time).abs() > (1.0 / (2.0 * FREQ)) { + if last_time != -1.0 { + current_frame += 1; + } + last_time = time; + } + + if current_frame >= minute_len { + continue; + } + + if let Some((role, jersey)) = player_map.get(person.person_id.he_id) { + for (&j_name, val) in joints_map { + if let JointValue::Pos(pos) = val { + let pid = person.person_id.he_id; + + let ix = + *col_cache.entry((pid, j_name, 'x')).or_insert_with(|| { + builder.get_or_create_column_id( + &format!( + "teams/{}/players/{}/landmarks/{}/x", + role, jersey, j_name + ), + DataType::Float32, + ) + }); + let iy = + *col_cache.entry((pid, j_name, 'y')).or_insert_with(|| { + builder.get_or_create_column_id( + &format!( + "teams/{}/players/{}/landmarks/{}/y", + role, jersey, j_name + ), + DataType::Float32, + ) + }); + let iz = + *col_cache.entry((pid, j_name, 'z')).or_insert_with(|| { + builder.get_or_create_column_id( + &format!( + "teams/{}/players/{}/landmarks/{}/z", + role, jersey, j_name + ), + DataType::Float32, + ) + }); + builder.insert_float_at_idx(current_frame, ix, pos[0] as f64); + builder.insert_float_at_idx(current_frame, iy, pos[1] as f64); + builder.insert_float_at_idx(current_frame, iz, pos[2] as f64); + + let ivis = + *col_cache.entry((pid, j_name, 'v')).or_insert_with(|| { + builder.get_or_create_column_id( + &format!( + "teams/{}/players/{}/landmarks/{}/is_visible", + role, jersey, j_name + ), + DataType::Boolean, + ) + }); + builder.insert_bool_at_idx(current_frame, ivis, true); + } + } + } + } + } + + Ok(builder) + }) + .collect(); + + for res in chunk_builders { + builders.push(res?); + } + } + + let final_builder = builders + .into_iter() + .reduce(|mut a, b| { + a.merge(b); + a + }) + .ok_or("No data loaded")?; + + let mut container = final_builder.build().map_err(|e| e.to_string())?; + + let mut x_range = None; + let mut y_range = None; + if let Some(path) = meta_path { + let meta_str = fs::read_to_string(path).map_err(|e| e.to_string())?; + let meta_data: HawkEyeMetaData = + serde_json::from_str(&meta_str).map_err(|e| e.to_string())?; + x_range = Some(( + -meta_data.stadium.pitch_length / 2.0, + meta_data.stadium.pitch_length / 2.0, + )); + y_range = Some(( + -meta_data.stadium.pitch_width / 2.0, + meta_data.stadium.pitch_width / 2.0, + )); + } + + let cs = CoordinateSystem::new( + x_range, + y_range, + Origin::Center, + Direction::Right, + Direction::Up, + Some(LengthUnit::Meters), + ); + container = container.with_coordinate_system(cs); + + Ok(container) +} diff --git a/crates/tdc-parsers/src/lib.rs b/crates/tdc-parsers/src/lib.rs index a029bd9..931c98f 100644 --- a/crates/tdc-parsers/src/lib.rs +++ b/crates/tdc-parsers/src/lib.rs @@ -1,6 +1,8 @@ +pub mod hawkeye; pub mod sportec; pub mod statsperform; +pub use hawkeye::load_hawkeye; pub use sportec::load_sportec; pub use statsperform::load_statsperform; @@ -34,9 +36,34 @@ pub mod python { PyCapsule::new(py, container, Some(name)) } + #[pyfunction] + #[pyo3( + name = "load_hawkeye", + signature = (ball_paths, centroid_paths, joint_paths=None, meta_path=None) + )] + pub fn load_hawkeye_py( + py: Python<'_>, + ball_paths: Vec, + centroid_paths: Vec, + joint_paths: Option>, + meta_path: Option, + ) -> PyResult> { + let container = super::load_hawkeye( + &ball_paths, + ¢roid_paths, + joint_paths.as_deref(), + meta_path.as_deref(), + ) + .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; + + let name = CString::new("TrackingDataContainer").unwrap(); + PyCapsule::new(py, container, Some(name)) + } + pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(load_sportec_py, m)?)?; m.add_function(wrap_pyfunction!(load_statsperform_py, m)?)?; + m.add_function(wrap_pyfunction!(load_hawkeye_py, m)?)?; Ok(()) } } diff --git a/crates/tdc-parsers/tests/test_hawkeye.rs b/crates/tdc-parsers/tests/test_hawkeye.rs new file mode 100644 index 0000000..9ae2c4b --- /dev/null +++ b/crates/tdc-parsers/tests/test_hawkeye.rs @@ -0,0 +1,257 @@ +use arrow_array::Array; +use tdc_parsers::load_hawkeye; + +#[test] +fn test_hawkeye_parser() { + let ball_content = r#"{ + "details": { + "teams": [ + {"id": {"heId": "team1"}, "name": "Team A", "home": true}, + {"id": {"heId": "team2"}, "name": "Team B", "home": false} + ] + }, + "samples": { + "ball": [ + {"pos": [1.0, 2.0, 3.0], "time": 0.0}, + {"pos": [4.0, 5.0, 6.0], "time": 0.04} + ] + } +}"#; + + let centroid_content = r#"{ + "details": { + "players": [ + {"id": {"heId": "p1"}, "teamId": {"heId": "team1"}, "jerseyNumber": "10"}, + {"id": {"heId": "p2"}, "teamId": {"heId": "team2"}, "jerseyNumber": "5"} + ] + }, + "samples": { + "people": [ + { + "personId": {"heId": "p1"}, + "role": {"id": 1, "name": "Outfielder"}, + "centroid": [{"pos": [10.0, 20.0], "time": 0.0}] + }, + { + "personId": {"heId": "p2"}, + "role": {"id": 1, "name": "Outfielder"}, + "centroid": [{"pos": [30.0, 40.0], "time": 0.0}] + }, + { + "personId": {"heId": "ref1"}, + "role": {"id": 4, "name": "Referee"}, + "centroid": [{"pos": [100.0, 200.0], "time": 0.0}] + }, + { + "personId": {"heId": "p1"}, + "role": {"id": 1, "name": "Outfielder"}, + "centroid": [{"pos": [40.0, 50.0], "time": 0.04}] + }, + { + "personId": {"heId": "p2"}, + "role": {"id": 1, "name": "Outfielder"}, + "centroid": [{"pos": [60.0, 70.0], "time": 0.04}] + } + ] + } +}"#; + + let joint_content = r#"{ + "samples": { + "people": [ + { + "personId": {"heId": "p1"}, + "joints": [ + { + "time": 0.0, + "lAnkle": [11.0, 21.0, 0.1], + "rAnkle": [12.0, 22.0, 0.2] + } + ] + }, + { + "personId": {"heId": "p1"}, + "joints": [ + { + "time": 0.04, + "lAnkle": [41.0, 51.0, 0.3], + "rAnkle": [42.0, 52.0, 0.4] + } + ] + } + ] + } +}"#; + + let meta_content = r#"{ + "Stadium": { + "PitchLength": 105.0, + "PitchWidth": 68.0 + } +}"#; + + // Use filenames that trigger period detection + let dir = tempfile::tempdir().unwrap(); + let ball_path = dir.path().join("match_1_1.football.samples.ball.json"); + let centroid_path = dir.path().join("match_1_1.football.samples.centroids.json"); + let joint_path = dir.path().join("match_1_1.football.samples.joints.json"); + let meta_path = dir.path().join("meta.json"); + + fs::write(&ball_path, ball_content).unwrap(); + fs::write(¢roid_path, centroid_content).unwrap(); + fs::write(&joint_path, joint_content).unwrap(); + fs::write(&meta_path, meta_content).unwrap(); + + let container = load_hawkeye( + &[ball_path.to_str().unwrap().to_string()], + &[centroid_path.to_str().unwrap().to_string()], + Some(&[joint_path.to_str().unwrap().to_string()]), + Some(meta_path.to_str().unwrap()), + ) + .unwrap(); + + // 1. Verify Schema and Field Mapping + let fields: Vec = container + .schema + .fields + .iter() + .map(|f| f.name().clone()) + .collect(); + + let expected_fields = vec![ + "ball/x", + "ball/y", + "ball/z", + "teams/home/players/10/x", + "teams/home/players/10/y", + "teams/home/players/10/landmarks/lAnkle/x", + "teams/home/players/10/landmarks/lAnkle/y", + "teams/home/players/10/landmarks/lAnkle/z", + "teams/home/players/10/landmarks/lAnkle/is_visible", + "teams/away/players/5/x", + "teams/away/players/5/y", + "referees/ref1/x", + "referees/ref1/y", + "period", + "vendor/tracking/name", + ]; + + for field in expected_fields { + assert!( + fields.contains(&field.to_string()), + "Missing field: {}", + field + ); + } + + // 2. Verify Data Types and Specific Values + let vendor_idx = container + .schema + .fields + .iter() + .position(|f| f.name() == "vendor/tracking/name") + .unwrap(); + let vendor_col = arrow_cast::cast( + container.data.column(vendor_idx), + &arrow_schema::DataType::Utf8, + ) + .unwrap(); + let vendor_val = vendor_col + .as_any() + .downcast_ref::() + .unwrap() + .value(0); + assert_eq!(vendor_val, "hawkeye"); + + let period_idx = container + .schema + .fields + .iter() + .position(|f| f.name() == "period") + .unwrap(); + let period_col = arrow_cast::cast( + container.data.column(period_idx), + &arrow_schema::DataType::Utf8, + ) + .unwrap(); + let period_val = period_col + .as_any() + .downcast_ref::() + .unwrap() + .value(0); + assert_eq!(period_val, "first_half"); + + assert_eq!(container.orientation, None); + + // 3. Verify Coordinate System + let cs = container + .coordinate_system + .expect("Coordinate system should be set"); + assert_eq!(cs.x_range, Some((-52.5, 52.5))); + assert_eq!(cs.y_range, Some((-34.0, 34.0))); + assert_eq!(cs.origin, tdc_core::schema::Origin::Center); + assert_eq!(cs.x_direction, tdc_core::schema::Direction::Right); + assert_eq!(cs.y_direction, tdc_core::schema::Direction::Up); + assert_eq!(cs.length_unit, Some(tdc_core::schema::LengthUnit::Meters)); + + // 4. Verify Entity Attributes + let check_float = |name: &str, row: usize, expected: f32| { + let idx = container + .schema + .fields + .iter() + .position(|f| f.name() == name) + .unwrap_or_else(|| panic!("Field {} not found", name)); + let col = container + .data + .column(idx) + .as_any() + .downcast_ref::() + .unwrap_or_else(|| panic!("Field {} is not Float32", name)); + assert!(!col.is_null(row), "Field {} is null at row {}", name, row); + assert!( + (col.value(row) - expected).abs() < 1e-6, + "Field {} value mismatch at row {}: expected {}, got {}", + name, + row, + expected, + col.value(row) + ); + }; + + check_float("ball/x", 0, 1.0); + check_float("ball/y", 0, 2.0); + check_float("ball/z", 0, 3.0); + check_float("teams/home/players/10/x", 0, 10.0); + check_float("teams/home/players/10/y", 0, 20.0); + check_float("teams/away/players/5/x", 0, 30.0); + check_float("teams/away/players/5/y", 0, 40.0); + check_float("referees/ref1/x", 0, 100.0); + check_float("referees/ref1/y", 0, 200.0); + + // Landmarks row 0 + check_float("teams/home/players/10/landmarks/lAnkle/x", 0, 11.0); + check_float("teams/home/players/10/landmarks/lAnkle/y", 0, 21.0); + check_float("teams/home/players/10/landmarks/lAnkle/z", 0, 0.1); + check_float("teams/home/players/10/landmarks/rAnkle/x", 0, 12.0); + check_float("teams/home/players/10/landmarks/rAnkle/y", 0, 22.0); + check_float("teams/home/players/10/landmarks/rAnkle/z", 0, 0.2); + + check_float("ball/x", 1, 4.0); + check_float("ball/y", 1, 5.0); + check_float("ball/z", 1, 6.0); + check_float("teams/home/players/10/x", 1, 40.0); + check_float("teams/home/players/10/y", 1, 50.0); + check_float("teams/away/players/5/x", 1, 60.0); + check_float("teams/away/players/5/y", 1, 70.0); + + // Landmarks row 1 + check_float("teams/home/players/10/landmarks/lAnkle/x", 1, 41.0); + check_float("teams/home/players/10/landmarks/lAnkle/y", 1, 51.0); + check_float("teams/home/players/10/landmarks/lAnkle/z", 1, 0.3); + check_float("teams/home/players/10/landmarks/rAnkle/x", 1, 42.0); + check_float("teams/home/players/10/landmarks/rAnkle/y", 1, 52.0); + check_float("teams/home/players/10/landmarks/rAnkle/z", 1, 0.4); +} + +use std::fs; diff --git a/crates/tdc-python/src/container.rs b/crates/tdc-python/src/container.rs index c32db11..8ab2f72 100644 --- a/crates/tdc-python/src/container.rs +++ b/crates/tdc-python/src/container.rs @@ -281,6 +281,11 @@ impl PyTrackingDataContainer { fn num_rows(&self) -> usize { self.inner.num_rows() } + fn slice(&self, offset: usize, length: usize) -> Self { + PyTrackingDataContainer { + inner: self.inner.slice(offset, length), + } + } fn validate(&self) -> PyResult<()> { self.inner.validate().into_pyresult() } @@ -404,6 +409,11 @@ impl PyLazyTrackingDataContainer { inner: self.inner.select(fields).into_pyresult()?, }) } + fn slice(&self, offset: usize, length: usize) -> PyResult { + Ok(PyLazyTrackingDataContainer { + inner: self.inner.slice(offset, length).into_pyresult()?, + }) + } fn filter(&self, predicate: PyExpr) -> PyResult { Ok(PyLazyTrackingDataContainer { inner: self.inner.filter(predicate.inner).into_pyresult()?, diff --git a/crates/tdc-python/src/selector.rs b/crates/tdc-python/src/selector.rs index 8120786..37a64e5 100644 --- a/crates/tdc-python/src/selector.rs +++ b/crates/tdc-python/src/selector.rs @@ -47,6 +47,11 @@ impl PyColumnSelector { expr: CoreExpr::Players { team_id }, } } + fn player(&self, player_id: String, team_id: Option) -> PySelection { + PySelection { + expr: CoreExpr::Player { team_id, player_id }, + } + } fn team(&self, team_id: &str) -> PySelection { PySelection { expr: CoreExpr::Players { diff --git a/examples/06-skeletal.ipynb b/examples/06-skeletal.ipynb new file mode 100644 index 0000000..540fb80 --- /dev/null +++ b/examples/06-skeletal.ipynb @@ -0,0 +1,226969 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Hawk-Eye Skeletal Tracking Data\n", + "\n", + "This notebook illustrates how to load Hawk-Eye tracking data, including full skeletal information (landmarks), and how to efficiently work with this high-dimensional data using `tracking-data-container`." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import re\n", + "import time\n", + "import psutil\n", + "from pathlib import Path\n", + "from typing import Union\n", + "import polars as pl\n", + "import tracking_data_container as tdc\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib.animation import FuncAnimation\n", + "from IPython.display import HTML\n", + "\n", + "def get_memory_usage():\n", + " process = psutil.Process(os.getpid())\n", + " return process.memory_info().rss / (1024 * 1024) # MB\n", + "\n", + "def _natural_sort_key(path: Union[str, Path]) -> list[Union[int, str]]:\n", + " # Split string into list of chunks for natural sorting\n", + " return [\n", + " int(text) if text.isdigit() else text.lower()\n", + " for text in re.split(r\"(\\d+)\", str(path))\n", + " ]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Load the full dataset\n", + "\n", + "We will load all 90+ minutes of data, including ball tracking, player centroids, and joint positions." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Found 95 minutes of data.\n", + "Loaded 132917 frames in 25.05 seconds.\n", + "Memory used: 3578.27 MB\n", + "Total columns: 3552\n" + ] + } + ], + "source": [ + "data_dir = Path(\"../data/hawkeye\")\n", + "ball_files = sorted(list(data_dir.glob(\"*.samples.ball.json\")), key=_natural_sort_key)\n", + "centroid_files = sorted(list(data_dir.glob(\"*.samples.centroids.json\")), key=_natural_sort_key)\n", + "joint_files = sorted(list(data_dir.glob(\"*.samples.joints.json\")), key=_natural_sort_key)\n", + "meta_file = data_dir / \"meta.json\"\n", + "\n", + "print(f\"Found {len(ball_files)} minutes of data.\")\n", + "\n", + "start_mem = get_memory_usage()\n", + "start_time = time.time()\n", + "\n", + "container = tdc.load_hawkeye(\n", + " ball_files, \n", + " centroid_files, \n", + " joint_paths=joint_files, \n", + " meta_path=meta_file\n", + ")\n", + "\n", + "end_time = time.time()\n", + "end_mem = get_memory_usage()\n", + "\n", + "print(f\"Loaded {container.num_rows()} frames in {end_time - start_time:.2f} seconds.\")\n", + "print(f\"Memory used: {end_mem - start_mem:.2f} MB\")\n", + "print(f\"Total columns: {len(container.schema().names())}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Convert to Parquet\n", + "\n", + "Parquet is a columnar storage format that is highly efficient for large datasets. `tracking-data-container` allows seamless export to Parquet, which preserves the standardized schema and metadata." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Data written to ./data/hawkeye_full.parquet\n", + "File size: 1646.18 MB\n" + ] + } + ], + "source": [ + "parquet_path = \"./data/hawkeye_full.parquet\"\n", + "container.to_parquet(parquet_path)\n", + "print(f\"Data written to {parquet_path}\")\n", + "print(f\"File size: {os.path.getsize(parquet_path) / (1024 * 1024):.2f} MB\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Efficient Loading for Specific Players\n", + "\n", + "One of the key advantages of using TDC with Parquet is the ability to lazily scan the data and only load the columns you actually need. Here we show how to load only the skeletal data for a single player." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded 118 columns for player #10 in 0.1026 seconds.\n", + "Memory used for selection: 71.14 MB\n" + ] + } + ], + "source": [ + "# Scan the Parquet file lazily\n", + "lazy_container = tdc.scan_parquet(parquet_path)\n", + "\n", + "# Select a specific player (e.g., player #10 on the home team)\n", + "player_selector = lazy_container.selector.player(\"10\", team_id=\"home\")\n", + "\n", + "start_mem = get_memory_usage()\n", + "start_time = time.time()\n", + "\n", + "# Collect the data for the selected player only\n", + "player_data = lazy_container.select(player_selector).collect()\n", + "\n", + "end_time = time.time()\n", + "end_mem = get_memory_usage()\n", + "\n", + "print(f\"Loaded {len(player_data.schema().names())} columns for player #10 in {end_time - start_time:.4f} seconds.\")\n", + "print(f\"Memory used for selection: {end_mem - start_mem:.2f} MB\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Animate 3D Pose Tracking\n", + "\n", + "Now we will animate the 3D pose tracking data for our selected player. We will define the skeletal connections and use `matplotlib` to render the animation." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
\n", + " \n", + "
\n", + " \n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + "
\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Define the skeletal connections for Hawk-Eye joints\n", + "CONNECTIONS = [\n", + " (\"nose\", \"lEye\"), (\"nose\", \"rEye\"),\n", + " (\"lEye\", \"lEar\"), (\"rEye\", \"rEar\"),\n", + " (\"neck\", \"lShoulder\"), (\"neck\", \"rShoulder\"),\n", + " (\"lShoulder\", \"rShoulder\"),\n", + " (\"lShoulder\", \"lElbow\"), (\"lElbow\", \"lWrist\"),\n", + " (\"rShoulder\", \"rElbow\"), (\"rElbow\", \"rWrist\"),\n", + " (\"lShoulder\", \"lHip\"), (\"rShoulder\", \"rHip\"),\n", + " (\"lHip\", \"rHip\"),\n", + " (\"lHip\", \"midHip\"), (\"rHip\", \"midHip\"),\n", + " (\"lHip\", \"lKnee\"), (\"lKnee\", \"lAnkle\"),\n", + " (\"rHip\", \"rKnee\"), (\"rKnee\", \"rAnkle\"),\n", + " (\"lAnkle\", \"lHeel\"), (\"lAnkle\", \"lBigToe\"), (\"lBigToe\", \"lSmallToe\"),\n", + " (\"rAnkle\", \"rHeel\"), (\"rAnkle\", \"rBigToe\"), (\"rBigToe\", \"rSmallToe\"),\n", + "]\n", + "\n", + "def animate_player(container, player_id, team=\"home\", start_frame=0, num_frames=150):\n", + " # Select player skeletal data and ball data\n", + " selector = [container.selector.player(player_id, team_id=team), container.selector.ball()]\n", + " data = container.slice(start_frame, num_frames).select(selector).to_polars()\n", + " \n", + " fig = plt.figure(figsize=(10, 8))\n", + " ax = fig.add_subplot(111, projection='3d')\n", + " \n", + " # Base prefix for player landmarks\n", + " prefix = f\"teams/{team}/players/{player_id}/landmarks/\"\n", + " \n", + " # Initialize lines for skeleton and point for ball\n", + " lines = [ax.plot([], [], [], color='blue', lw=2)[0] for _ in CONNECTIONS]\n", + " ball_point, = ax.plot([], [], [], 'ro', markersize=10, label=\"Ball\")\n", + " \n", + " # Determine limits based on player position\n", + " player_x = data.get_column(f\"{prefix}midHip/x\").mean()\n", + " player_y = data.get_column(f\"{prefix}midHip/y\").mean()\n", + " \n", + " ax.set_xlim(player_x - 3, player_x + 3)\n", + " ax.set_ylim(player_y - 3, player_y + 3)\n", + " ax.set_zlim(0, 3)\n", + " ax.set_xlabel(\"X (m)\")\n", + " ax.set_ylabel(\"Y (m)\")\n", + " ax.set_zlabel(\"Z (m)\")\n", + " ax.set_title(f\"Player {player_id} ({team}) - 3D Pose Animation\")\n", + " \n", + " def update(frame):\n", + " row = data.row(frame, named=True)\n", + " \n", + " # Update skeleton lines\n", + " for i, (start, end) in enumerate(CONNECTIONS):\n", + " try:\n", + " x = [row[f\"{prefix}{start}/x\"], row[f\"{prefix}{end}/x\"]]\n", + " y = [row[f\"{prefix}{start}/y\"], row[f\"{prefix}{end}/y\"]]\n", + " z = [row[f\"{prefix}{start}/z\"], row[f\"{prefix}{end}/z\"]]\n", + " lines[i].set_data(x, y)\n", + " lines[i].set_3d_properties(z)\n", + " except KeyError:\n", + " continue\n", + " \n", + " # Update ball position\n", + " ball_point.set_data([row[\"ball/x\"]], [row[\"ball/y\"]])\n", + " ball_point.set_3d_properties([row[\"ball/z\"]])\n", + " \n", + " return lines + [ball_point]\n", + "\n", + " anim = FuncAnimation(fig, update, frames=num_frames, interval=40, blit=True)\n", + " plt.close(fig)\n", + " return anim\n", + "\n", + "# Animate player #10 for 6 seconds (150 frames at 25Hz)\n", + "anim = animate_player(container, \"10\", team=\"home\", start_frame=1500, num_frames=150)\n", + "HTML(anim.to_jshtml())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "/Users/probberechts/Developer/tracking_data_container", + "language": "python", + "name": "tracking_data_container" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.14" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python/tests/benchmarks/test_hawkeye.py b/python/tests/benchmarks/test_hawkeye.py new file mode 100644 index 0000000..3b8dd3c --- /dev/null +++ b/python/tests/benchmarks/test_hawkeye.py @@ -0,0 +1,108 @@ +import re +import tracemalloc +from pathlib import Path +from typing import Union + +import polars as pl +import pytest +import tracking_data_container as tdc + +try: + from kloppy import hawkeye + + KLOPPY_AVAILABLE = True +except ImportError: + KLOPPY_AVAILABLE = False + +try: + import fastforward as fff + + _ = fff.hawkeye + FFF_AVAILABLE = True +except (ImportError, AttributeError): + FFF_AVAILABLE = False + + +def _natural_sort_key(path: Union[str, Path]) -> list[Union[int, str]]: + # Split string into list of chunks for natural sorting + return [int(text) if text.isdigit() else text.lower() for text in re.split(r"(\d+)", str(path))] + + +HAWKEYE_DIR = Path(__file__).parents[3] / "data" / "hawkeye" +# Limit to 5 minutes for benchmarks to keep it reasonable +BALL_FILES = sorted(list(HAWKEYE_DIR.glob("*.samples.ball.json")), key=_natural_sort_key)[:5] +CENTROID_FILES = sorted(list(HAWKEYE_DIR.glob("*.samples.centroids.json")), key=_natural_sort_key)[ + :5 +] +JOINT_FILES = sorted(list(HAWKEYE_DIR.glob("*.samples.joints.json")), key=_natural_sort_key)[:5] +HAWKEYE_META = HAWKEYE_DIR / "meta.json" + + +@pytest.mark.benchmark(group="hawkeye") +def test_hawkeye_tdc_parsers(benchmark): + def run(): + return tdc.load_hawkeye( + BALL_FILES, CENTROID_FILES, joint_paths=JOINT_FILES, meta_path=HAWKEYE_META + ).to_polars() + + tracemalloc.start() + # Using pedantic to be consistent with others + df = benchmark.pedantic(run, rounds=5, iterations=1) + _, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + + benchmark.extra_info["peak_memory_mb"] = peak / 1024 / 1024 + + assert isinstance(df, pl.DataFrame) + assert len(df) > 0 + + +@pytest.mark.skipif(not KLOPPY_AVAILABLE, reason="kloppy not installed") +@pytest.mark.benchmark(group="hawkeye") +def test_hawkeye_kloppy(benchmark): + def run(): + # Kloppy's hawkeye loader expects ball_feeds and player_centroid_feeds + # Note: Kloppy might not support joint feeds yet or uses different args. + # Based on previous research, it only supported ball and centroids. + dataset = hawkeye.load( + ball_feeds=[str(p) for p in BALL_FILES], + player_centroid_feeds=[str(p) for p in CENTROID_FILES], + meta_data=str(HAWKEYE_META), + ) + return dataset + + tracemalloc.start() + # Kloppy is slow, we use pedantic to limit rounds + dataset = benchmark.pedantic(run, rounds=1, iterations=1) + _, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + + benchmark.extra_info["peak_memory_mb"] = peak / 1024 / 1024 + + assert dataset is not None + + +@pytest.mark.skipif(not FFF_AVAILABLE, reason="fastforward.hawkeye not available") +@pytest.mark.benchmark(group="hawkeye") +def test_hawkeye_fff(benchmark): + def run(): + # Fastforward supports joint_data + dataset = fff.hawkeye.load_tracking( + ball_data=[str(p) for p in BALL_FILES], + player_data=[str(p) for p in CENTROID_FILES], + meta_data=str(HAWKEYE_META), + layout="wide", + ) + # Use .tracking which is typically the Polars DataFrame in fff + return dataset.tracking + + tracemalloc.start() + # Fastforward can be slow, we use pedantic to limit rounds + df = benchmark.pedantic(run, rounds=1, iterations=1) + _, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + + benchmark.extra_info["peak_memory_mb"] = peak / 1024 / 1024 + + assert isinstance(df, pl.DataFrame) + assert len(df) > 0 diff --git a/python/tests/benchmarks/test_lazy_filtering.py b/python/tests/benchmarks/test_lazy_filtering.py index d2ae791..2081c96 100644 --- a/python/tests/benchmarks/test_lazy_filtering.py +++ b/python/tests/benchmarks/test_lazy_filtering.py @@ -5,7 +5,7 @@ import pytest import tracking_data_container as tdc -STATSPERFORM_FILE = Path(__file__).parents[3] / "data" / "statsperform-ma25.txt" +STATSPERFORM_FILE = Path(__file__).parents[3] / "data" / "statsperform" / "ma25.txt" @pytest.fixture(scope="session") diff --git a/python/tests/benchmarks/test_sportec.py b/python/tests/benchmarks/test_sportec.py index 8841d50..f489309 100644 --- a/python/tests/benchmarks/test_sportec.py +++ b/python/tests/benchmarks/test_sportec.py @@ -21,8 +21,8 @@ except (ImportError, AttributeError): FFF_AVAILABLE = False -SPORTEC_FILE = Path(__file__).parents[3] / "data" / "sportec-tracking.xml" -SPORTEC_META = Path(__file__).parents[3] / "data" / "sportec-meta.xml" +SPORTEC_FILE = Path(__file__).parents[3] / "data" / "sportec" / "tracking.xml" +SPORTEC_META = Path(__file__).parents[3] / "data" / "sportec" / "meta.xml" def tdc_parse_sportec(path: Path) -> pl.DataFrame: diff --git a/python/tests/benchmarks/test_statsperform.py b/python/tests/benchmarks/test_statsperform.py index 11f4c3e..7f38e41 100644 --- a/python/tests/benchmarks/test_statsperform.py +++ b/python/tests/benchmarks/test_statsperform.py @@ -20,8 +20,8 @@ except (ImportError, AttributeError): FFF_AVAILABLE = False -STATSPERFORM_FILE = Path(__file__).parents[3] / "data" / "statsperform-ma25.txt" -STATSPERFORM_META = Path(__file__).parents[3] / "data" / "statsperform-ma1.json" +STATSPERFORM_FILE = Path(__file__).parents[3] / "data" / "statsperform" / "ma25.txt" +STATSPERFORM_META = Path(__file__).parents[3] / "data" / "statsperform" / "ma1.json" def tdc_parse_statsperform(path: Path) -> pl.DataFrame: diff --git a/python/tests/test_select.py b/python/tests/test_select.py index 3d8315a..ec7be6b 100644 --- a/python/tests/test_select.py +++ b/python/tests/test_select.py @@ -35,6 +35,25 @@ def test_container_select_selector_players(self, container) -> None: subset = c.select(c.selector.players().attributes(["x"])) assert subset.schema() == tdc.Fields(["teams/away/players/10/x", "teams/home/players/1/x"]) + def test_container_select_selector_player(self) -> None: + data = { + "teams/home/players/1/x": [2.0], + "teams/home/players/1/y": [3.0], + "teams/home/players/2/x": [4.0], + "teams/away/players/1/x": [5.0], + } + c = tdc.TrackingDataContainer(data) + + # Select specific player by ID and team + subset = c.select(c.selector.player("1", team_id="home")) + assert subset.schema() == tdc.Fields(["teams/home/players/1/x", "teams/home/players/1/y"]) + + # Select specific player by ID only + subset = c.select(c.selector.player("1")) + assert subset.schema() == tdc.Fields( + ["teams/away/players/1/x", "teams/home/players/1/x", "teams/home/players/1/y"] + ) + def test_container_select_wildcard(self) -> None: data = { "ball/x": [1.0], diff --git a/python/tracking_data_container/__init__.py b/python/tracking_data_container/__init__.py index 8a59eb1..aeb2db0 100644 --- a/python/tracking_data_container/__init__.py +++ b/python/tracking_data_container/__init__.py @@ -17,7 +17,7 @@ from .builder import TrackingDataBuilder from .container import LazyTrackingDataContainer, TrackingDataContainer from .coordinate_system import CoordinateSystem -from .io import load_sportec, load_statsperform, scan_parquet +from .io import load_hawkeye, load_sportec, load_statsperform, scan_parquet from .schema import ( Ball, DataField, @@ -46,6 +46,7 @@ "scan_parquet", "load_sportec", "load_statsperform", + "load_hawkeye", "DataField", "TrackedEntity", "Ball", diff --git a/python/tracking_data_container/_tracking_data_container.pyi b/python/tracking_data_container/_tracking_data_container.pyi index 9efdad4..c9a95f3 100644 --- a/python/tracking_data_container/_tracking_data_container.pyi +++ b/python/tracking_data_container/_tracking_data_container.pyi @@ -154,6 +154,7 @@ class ColumnSelector: """A selector for building column expressions based on metadata.""" def ball(self) -> Selection: ... def players(self, team_id: str | None = None) -> Selection: ... + def player(self, player_id: str, team_id: str | None = None) -> Selection: ... def team(self, team_id: str) -> Selection: ... class Frame: @@ -194,6 +195,7 @@ class _TrackingDataContainer: @property def selector(self) -> ColumnSelector: ... def num_rows(self) -> int: ... + def slice(self, offset: int, length: int) -> _TrackingDataContainer: ... def validate(self) -> None: ... def save_parquet(self, path: str) -> None: ... @staticmethod @@ -209,6 +211,7 @@ class _LazyTrackingDataContainer: def select( self, items: str | list[str | _DataField | _Selection] | _DataField | _Selection ) -> _LazyTrackingDataContainer: ... + def slice(self, offset: int, length: int) -> _LazyTrackingDataContainer: ... def filter(self, predicate: Expr) -> _LazyTrackingDataContainer: ... def transform_coordinates(self, to_system: _CoordinateSystem) -> _LazyTrackingDataContainer: ... def transform_orientation(self, mode: OrientationMode | str) -> _LazyTrackingDataContainer: ... @@ -261,8 +264,14 @@ class Expr: def col(name: str) -> Expr: ... def lit(value: Any) -> Expr: ... -def load_sportec(path: str, meta_path: str | None = None) -> _TrackingDataContainer: ... -def load_statsperform(path: str) -> _TrackingDataContainer: ... +def load_sportec(path: str, meta_path: str | None = None) -> Any: ... +def load_statsperform(path: str) -> Any: ... +def load_hawkeye( + ball_paths: list[str], + centroid_paths: list[str], + joint_paths: list[str] | None = None, + meta_path: str | None = None, +) -> Any: ... def scan_parquet(path: str) -> _LazyTrackingDataContainer: ... class _Ops: diff --git a/python/tracking_data_container/container.py b/python/tracking_data_container/container.py index 3311eda..7b3a924 100644 --- a/python/tracking_data_container/container.py +++ b/python/tracking_data_container/container.py @@ -166,6 +166,27 @@ def __getitem__(self, key: int) -> Frame: """Access a specific frame by index.""" return self.materialize_frame(key) + def slice(self, offset: int, length: int) -> TrackingDataContainer: + """ + Extract a slice of frames from the container. + + Parameters + ---------- + offset : int + The starting frame index. + length : int + The number of frames to include. + + Returns + ------- + TrackingDataContainer + A new container containing only the specified frames. + """ + new_inner = self._container.slice(offset, length) + new_container = TrackingDataContainer.__new__(TrackingDataContainer) + new_container._container = new_inner + return new_container + # --- Transformations --- def with_coordinate_system(self, system: CoordinateSystem) -> TrackingDataContainer: @@ -826,6 +847,25 @@ def select(self, items: str | list[str | DataField | Selection]) -> LazyTracking new_inner = self._container.select(processed_items) # type: ignore return LazyTrackingDataContainer(new_inner) + def slice(self, offset: int, length: int) -> LazyTrackingDataContainer: + """ + Lazily extract a slice of frames from the container. + + Parameters + ---------- + offset : int + The starting frame index. + length : int + The number of frames to include. + + Returns + ------- + LazyTrackingDataContainer + A new lazy container with the slice added to the plan. + """ + new_inner = self._container.slice(offset, length) + return LazyTrackingDataContainer(new_inner) + def filter(self, predicate: _Expr) -> LazyTrackingDataContainer: """ Lazily filter rows from the container. diff --git a/python/tracking_data_container/io.py b/python/tracking_data_container/io.py index bff68e0..36aa3e2 100644 --- a/python/tracking_data_container/io.py +++ b/python/tracking_data_container/io.py @@ -6,6 +6,9 @@ from ._tracking_data_container import ( _TrackingDataContainer, ) +from ._tracking_data_container import ( + load_hawkeye as _load_hawkeye, +) from ._tracking_data_container import ( load_sportec as _load_sportec, ) @@ -75,3 +78,37 @@ def load_statsperform(path: str | Path) -> TrackingDataContainer: """ new_inner = _load_statsperform(str(path)) return TrackingDataContainer(_TrackingDataContainer._from_capsule(new_inner)) + + +def load_hawkeye( + ball_paths: list[str | Path], + centroid_paths: list[str | Path], + joint_paths: list[str | Path] | None = None, + meta_path: str | Path | None = None, +) -> TrackingDataContainer: + """ + Loads Hawk-Eye tracking files into an eager container. + + Parameters + ---------- + ball_paths : list of str or Path + The paths to the Hawk-Eye ball tracking data files. + centroid_paths : list of str or Path + The paths to the Hawk-Eye player centroid data files. + joint_paths : list of str or Path, optional + The paths to the Hawk-Eye player joint data files. + meta_path : str or Path, optional + The path to the Hawk-Eye metadata JSON file. + + Returns + ------- + TrackingDataContainer + An eager container containing the loaded data. + """ + new_inner = _load_hawkeye( + [str(p) for p in ball_paths], + [str(p) for p in centroid_paths], + [str(p) for p in joint_paths] if joint_paths else None, + str(meta_path) if meta_path else None, + ) + return TrackingDataContainer(_TrackingDataContainer._from_capsule(new_inner)) diff --git a/python/tracking_data_container/selector.py b/python/tracking_data_container/selector.py index cf5b74c..4f04b0a 100644 --- a/python/tracking_data_container/selector.py +++ b/python/tracking_data_container/selector.py @@ -57,6 +57,10 @@ def players(self, team_id: str | None = None) -> Selection: """Selects all player-related columns.""" return Selection(self._selector.players(team_id)) + def player(self, player_id: str, team_id: str | None = None) -> Selection: + """Selects all columns for a specific player.""" + return Selection(self._selector.player(player_id, team_id)) + def team(self, team_id: str) -> Selection: """Selects all columns for a specific team.""" return Selection(self._selector.team(team_id)) From e2715c466406dfd5e5b323e00031df83adc0b060 Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Wed, 8 Apr 2026 13:33:13 +0200 Subject: [PATCH 04/21] feat: implement high-performance Rust pivot for catalog loading --- GEMINI.md | 50 + crates/tdc-core/src/datacontainer/core.rs | 426 ++++- crates/tdc-core/src/schema/attribute.rs | 7 +- crates/tdc-python/src/container.rs | 70 +- crates/tdc-python/src/selector.rs | 13 + debug_warehouse/catalog.db | Bin 0 -> 20480 bytes ...-f000-4b09-aa96-69d2d17151c7.metadata.json | 1 + ...-1069-48a0-9f73-0f5337b4fa63.metadata.json | 1 + ...80fb8d-8d7d-435b-95e1-55f2b9ea7dd3-m0.avro | Bin 0 -> 4474 bytes ...-4f80fb8d-8d7d-435b-95e1-55f2b9ea7dd3.avro | Bin 0 -> 1819 bytes ...-14bf-436f-9ed2-cbbfb2a553b8.metadata.json | 1 + ...-69bc-4280-800d-ab07ceb3c869.metadata.json | 1 + ...25fa0c-bd68-4ba7-bfc2-9a6c8f662a6d-m0.avro | Bin 0 -> 5356 bytes ...-e525fa0c-bd68-4ba7-bfc2-9a6c8f662a6d.avro | Bin 0 -> 1829 bytes examples/07-catalog.ipynb | 320 ++++ examples/08-idsse-warehouse.ipynb | 376 +++++ examples/idsse_warehouse/catalog.db | Bin 0 -> 20480 bytes ...-c746-4f8f-966b-040290fbf4fa.metadata.json | 1 + ...-aa86-4054-844c-b33bbaf9b53b.metadata.json | 1 + ...-e21d-47a5-9d6f-6bc39ac8c361.metadata.json | 1 + ...-c7bf-4e2f-8e10-522378371fe8.metadata.json | 1 + ...defa57-62bd-4a60-87b5-0d6fcafd53ad-m0.avro | Bin 0 -> 4490 bytes ...38ec79-f365-46e1-aa0a-6ff5e030781a-m0.avro | Bin 0 -> 4489 bytes ...ae9c91-7f9c-4bec-b401-856370f90b74-m0.avro | Bin 0 -> 4492 bytes ...-ed38ec79-f365-46e1-aa0a-6ff5e030781a.avro | Bin 0 -> 1900 bytes ...-b3defa57-62bd-4a60-87b5-0d6fcafd53ad.avro | Bin 0 -> 1824 bytes ...-faae9c91-7f9c-4bec-b401-856370f90b74.avro | Bin 0 -> 1949 bytes ...-6439-4604-88e4-272cf35f75ce.metadata.json | 1 + ...-a73e-4da6-8812-af9a7d6b04e9.metadata.json | 1 + ...-fa71-41cb-a109-30c7bf7cadf4.metadata.json | 1 + ...-4237-409f-9d8b-49218b2e1ef4.metadata.json | 1 + ...65052f-9f33-4c4a-b55d-e1fd2bcb223d-m0.avro | Bin 0 -> 6329 bytes ...d1e2c6-167d-47a8-a02b-391e6ef92b65-m0.avro | Bin 0 -> 6347 bytes ...62db46-093c-45e8-be4a-0c6d572dad92-m0.avro | Bin 0 -> 6365 bytes ...-d962db46-093c-45e8-be4a-0c6d572dad92.avro | Bin 0 -> 1851 bytes ...-4665052f-9f33-4c4a-b55d-e1fd2bcb223d.avro | Bin 0 -> 2000 bytes ...-7ad1e2c6-167d-47a8-a02b-391e6ef92b65.avro | Bin 0 -> 1937 bytes inspect_catalog.py | 26 + profile_load.py | 146 ++ profile_warehouse/catalog.db | Bin 0 -> 20480 bytes ...-65e0-450f-9be7-7684faeeac52.metadata.json | 1 + ...-89c7-436e-83d6-51ccb7009828.metadata.json | 1 + ...7f5ba5-48f7-4cb2-ae56-16a5fe7fd851-m0.avro | Bin 0 -> 4470 bytes ...-e27f5ba5-48f7-4cb2-ae56-16a5fe7fd851.avro | Bin 0 -> 1819 bytes ...-7045-4467-ab11-81db388158b2.metadata.json | 1 + ...-eb36-4de8-8147-cabad48bca68.metadata.json | 1 + ...3266b5-4cd0-499d-b61d-580fce440e8d-m0.avro | Bin 0 -> 5402 bytes ...-f03266b5-4cd0-499d-b61d-580fce440e8d.avro | Bin 0 -> 1829 bytes pyproject.toml | 5 + .../benchmarks/test_catalog_performance.py | 68 + python/tests/test_advanced_features.py | 98 ++ python/tests/test_catalog.py | 71 + python/tests/test_ops.py | 2 +- python/tests/test_select.py | 6 +- python/tests/test_transform.py | 32 +- python/tracking_data_container/__init__.py | 12 + .../_tracking_data_container.pyi | 24 +- python/tracking_data_container/catalog.py | 543 ++++++ python/tracking_data_container/config.py | 25 + python/tracking_data_container/container.py | 1479 ++++++----------- python/tracking_data_container/schema.py | 143 +- uv.lock | 996 ++++++++++- 62 files changed, 3873 insertions(+), 1081 deletions(-) create mode 100644 GEMINI.md create mode 100644 debug_warehouse/catalog.db create mode 100644 debug_warehouse/tracking/match_metadata/metadata/00000-140c93eb-f000-4b09-aa96-69d2d17151c7.metadata.json create mode 100644 debug_warehouse/tracking/match_metadata/metadata/00001-702060e8-1069-48a0-9f73-0f5337b4fa63.metadata.json create mode 100644 debug_warehouse/tracking/match_metadata/metadata/4f80fb8d-8d7d-435b-95e1-55f2b9ea7dd3-m0.avro create mode 100644 debug_warehouse/tracking/match_metadata/metadata/snap-8457677481870634211-0-4f80fb8d-8d7d-435b-95e1-55f2b9ea7dd3.avro create mode 100644 debug_warehouse/tracking/tracking_data/metadata/00000-b6ef36f9-14bf-436f-9ed2-cbbfb2a553b8.metadata.json create mode 100644 debug_warehouse/tracking/tracking_data/metadata/00001-32b80343-69bc-4280-800d-ab07ceb3c869.metadata.json create mode 100644 debug_warehouse/tracking/tracking_data/metadata/e525fa0c-bd68-4ba7-bfc2-9a6c8f662a6d-m0.avro create mode 100644 debug_warehouse/tracking/tracking_data/metadata/snap-6099500306663132840-0-e525fa0c-bd68-4ba7-bfc2-9a6c8f662a6d.avro create mode 100644 examples/07-catalog.ipynb create mode 100644 examples/08-idsse-warehouse.ipynb create mode 100644 examples/idsse_warehouse/catalog.db create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00000-9af3a354-c746-4f8f-966b-040290fbf4fa.metadata.json create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00001-dc033d34-aa86-4054-844c-b33bbaf9b53b.metadata.json create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00002-2c7bc7d1-e21d-47a5-9d6f-6bc39ac8c361.metadata.json create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00003-16f8ad09-c7bf-4e2f-8e10-522378371fe8.metadata.json create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/b3defa57-62bd-4a60-87b5-0d6fcafd53ad-m0.avro create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/ed38ec79-f365-46e1-aa0a-6ff5e030781a-m0.avro create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/faae9c91-7f9c-4bec-b401-856370f90b74-m0.avro create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/snap-1114700784421547592-0-ed38ec79-f365-46e1-aa0a-6ff5e030781a.avro create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/snap-1456969541151746380-0-b3defa57-62bd-4a60-87b5-0d6fcafd53ad.avro create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/snap-4866667276096503338-0-faae9c91-7f9c-4bec-b401-856370f90b74.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00000-c6360589-6439-4604-88e4-272cf35f75ce.metadata.json create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00001-4bae733e-a73e-4da6-8812-af9a7d6b04e9.metadata.json create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00002-c4c023b8-fa71-41cb-a109-30c7bf7cadf4.metadata.json create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00003-53fc18be-4237-409f-9d8b-49218b2e1ef4.metadata.json create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/4665052f-9f33-4c4a-b55d-e1fd2bcb223d-m0.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/7ad1e2c6-167d-47a8-a02b-391e6ef92b65-m0.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/d962db46-093c-45e8-be4a-0c6d572dad92-m0.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-377391038018914117-0-d962db46-093c-45e8-be4a-0c6d572dad92.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4761525111313210917-0-4665052f-9f33-4c4a-b55d-e1fd2bcb223d.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-478866493446175361-0-7ad1e2c6-167d-47a8-a02b-391e6ef92b65.avro create mode 100644 inspect_catalog.py create mode 100644 profile_load.py create mode 100644 profile_warehouse/catalog.db create mode 100644 profile_warehouse/tracking/match_metadata/metadata/00000-4a6aa9a1-65e0-450f-9be7-7684faeeac52.metadata.json create mode 100644 profile_warehouse/tracking/match_metadata/metadata/00001-9b21cef5-89c7-436e-83d6-51ccb7009828.metadata.json create mode 100644 profile_warehouse/tracking/match_metadata/metadata/e27f5ba5-48f7-4cb2-ae56-16a5fe7fd851-m0.avro create mode 100644 profile_warehouse/tracking/match_metadata/metadata/snap-2219873109471251602-0-e27f5ba5-48f7-4cb2-ae56-16a5fe7fd851.avro create mode 100644 profile_warehouse/tracking/tracking_data/metadata/00000-b3a6aabe-7045-4467-ab11-81db388158b2.metadata.json create mode 100644 profile_warehouse/tracking/tracking_data/metadata/00001-6f0b9eff-eb36-4de8-8147-cabad48bca68.metadata.json create mode 100644 profile_warehouse/tracking/tracking_data/metadata/f03266b5-4cd0-499d-b61d-580fce440e8d-m0.avro create mode 100644 profile_warehouse/tracking/tracking_data/metadata/snap-3010651843225263066-0-f03266b5-4cd0-499d-b61d-580fce440e8d.avro create mode 100644 python/tests/benchmarks/test_catalog_performance.py create mode 100644 python/tests/test_advanced_features.py create mode 100644 python/tests/test_catalog.py create mode 100644 python/tracking_data_container/catalog.py create mode 100644 python/tracking_data_container/config.py diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..e37c36d --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,50 @@ +# Gemini Project: tracking-data-container + +This document provides a comprehensive overview of the `tracking-data-container` (TDC) project, designed to facilitate understanding and development. + +## Project Overview + +The `tracking-data-container` is a high-performance, metadata-aware container for sports tracking data. It implements the vision of a shared, standardized interface for the sports analytics ecosystem, backed by Apache Arrow and DataFusion. + +### Motivation & RFC +TDC aims to solve the challenges of fragmentation and duplication in sports tracking pipelines. It acts as the central interface between various tools (e.g., Kloppy, Floodlight, BallRadar), providing: +- **Scalability**: Overcoming the limitations of object-based models for large datasets. +- **Interoperability**: Standardized schemas and zero-copy data exchange. +- **Reproducibility**: Shared logic for derived metrics and transformations. + +### Core Architecture +- **Rust Core (`tdc-core`)**: Leverages Apache Arrow for in-memory data representation and DataFusion for query execution. It handles performance-critical operations like coordinate transformations and orientation normalization. +- **Python Bindings (`tdc-python`)**: Provides a high-level API for Python users, ensuring seamless integration with the data science ecosystem (Pandas, Polars, PyArrow). +- **Schema & CDF**: TDC aligns with the **Common Data Format (CDF)** standard for column naming (e.g., `ball/x`, `teams/{tid}/players/{pid}/x`). + +### Key Components +- **TrackingDataContainer**: The primary eager interface wrapping an Arrow `StructArray` and rich metadata. +- **LazyTrackingDataContainer**: A deferred execution model using DataFusion logical plans for lazy evaluation and filter pushdown. +- **DataField**: A domain-specific representation of a data column, supporting both hierarchical string paths and component-based construction. +- **TrackingDataBuilder**: An optimized ingestor for building containers from raw streams or diverse formats. +- **Frame**: A materialized single frame of data, supporting dual indexing by string name or `DataField`. + +## Building and Running + +The project uses a `Makefile` to streamline common development tasks. + +### Installation +```bash +make setup-dev +``` + +### Building +- **Development**: `make build-dev` +- **Production Wheel**: `make build-wheel` + +### Testing +- **Full Suite**: `make test` +- **Python Only**: `make test-python` +- **Rust Only**: `make test-rust` + +## Development Conventions + +- **Code Style**: `ruff` for Python, `cargo fmt` for Rust. +- **Type Hinting**: Full type hinting required for Python code (`mypy` compliant). +- **Metadata Handling**: Always prefer metadata-aware selectors (`container.selector`) over hardcoded strings. +- **Schema Compliance**: All changes should maintain compatibility with the CDF v0.2.2 specification. diff --git a/crates/tdc-core/src/datacontainer/core.rs b/crates/tdc-core/src/datacontainer/core.rs index 90cb2c1..45e166f 100644 --- a/crates/tdc-core/src/datacontainer/core.rs +++ b/crates/tdc-core/src/datacontainer/core.rs @@ -1,14 +1,34 @@ use crate::error::{TrackingDataContainerErrorKind, TrackingDataContainerResult}; -use crate::schema::{CoordinateSystem, DataField, Fields, OrientationMode}; +use crate::schema::{CoordinateSystem, DataField, Fields, OrientationMode, TrackedEntity}; use arrow_arith::numeric::{add, mul}; use arrow_array::array::{Array, ArrayRef, Float32Array, Float64Array, StructArray}; +use arrow_array::cast::AsArray; use arrow_array::{RecordBatch, Scalar}; use arrow_ord::cmp::eq; -use arrow_schema::{DataType::*, Field, Fields as ArrowFields}; +use arrow_schema::{DataType, DataType::*, Field, Fields as ArrowFields}; use arrow_select::zip::zip; use datafusion::prelude::*; use std::sync::Arc; +#[derive(Debug)] +pub struct TrackingDataContainer { + pub data: StructArray, + pub schema: Fields, + pub coordinate_system: Option, + pub orientation: Option, +} + +impl Clone for TrackingDataContainer { + fn clone(&self) -> Self { + Self { + data: self.data.clone(), + schema: self.schema.clone(), + coordinate_system: self.coordinate_system.clone(), + orientation: self.orientation, + } + } +} + impl TrackingDataContainer { pub fn new(data: StructArray, schema: Fields) -> Self { Self { @@ -59,42 +79,7 @@ impl TrackingDataContainer { self.orientation, )) } -} - -impl crate::selector::Select for TrackingDataContainer { - fn select(&self, fields: Vec) -> TrackingDataContainerResult { - let indices = self.schema.get_column_indices(&fields); - - let mut new_arrays = Vec::with_capacity(fields.len()); - let mut new_arrow_fields = Vec::with_capacity(fields.len()); - let mut new_schema_fields = Vec::with_capacity(fields.len()); - - for (field, maybe_idx) in fields.into_iter().zip(indices.into_iter()) { - if let Some(idx) = maybe_idx { - new_arrays.push(self.data.column(idx).clone()); - new_arrow_fields.push(self.data.fields()[idx].clone()); - new_schema_fields.push(field); - } - } - - let new_data = if new_arrays.is_empty() { - StructArray::new_empty_fields(self.num_rows(), None) - } else { - StructArray::try_new(ArrowFields::from(new_arrow_fields), new_arrays, None)? - }; - - Ok(Self { - data: new_data, - schema: Fields { - fields: new_schema_fields, - }, - coordinate_system: self.coordinate_system.clone(), - orientation: self.orientation, - }) - } -} -impl TrackingDataContainer { pub fn with_columns(&self, fields: Vec, arrays: Vec) -> Self { let mut new_fields: Vec> = self.data.fields().iter().cloned().collect(); let mut new_columns: Vec = self.data.columns().to_vec(); @@ -102,7 +87,7 @@ impl TrackingDataContainer { let mut field_map: std::collections::HashMap = new_fields .iter() .enumerate() - .map(|(i, f)| (f.name().clone(), i)) + .map(|(i, f): (usize, &Arc)| (f.name().clone(), i)) .collect(); for (field, array) in fields.into_iter().zip(arrays.into_iter()) { @@ -172,10 +157,8 @@ impl TrackingDataContainer { let mut new_cols = self.data.columns().to_vec(); let mut new_fields = self.data.fields().iter().cloned().collect::>(); - // Pre-calculate transformation coefficients and hoist scalars let (m_x, o_x) = from.get_transform_coeffs(&to_system, true)?; let (m_y, o_y) = from.get_transform_coeffs(&to_system, false)?; - // For Z, we use the same scale as Y for now, or X. Most sports tracking Z is meters if X/Y are meters. let (m_z, o_z) = (m_y, o_y); let m_x_f64 = Scalar::new(Float64Array::from(vec![m_x])); @@ -287,16 +270,15 @@ impl TrackingDataContainer { &arrow_arith::boolean::and_not(&target_mask, ¤t_mask)?, )?; - // Hoist flip scalars let (m_x, o_x) = self .coordinate_system .as_ref() - .map(|cs| cs.get_flip_coeffs(true)) + .map(|cs: &CoordinateSystem| cs.get_flip_coeffs(true)) .unwrap_or((-1.0, 0.0)); let (m_y, o_y) = self .coordinate_system .as_ref() - .map(|cs| cs.get_flip_coeffs(false)) + .map(|cs: &CoordinateSystem| cs.get_flip_coeffs(false)) .unwrap_or((-1.0, 0.0)); let m_x_f64 = Scalar::new(Float64Array::from(vec![m_x])); @@ -387,25 +369,357 @@ impl TrackingDataContainer { _ => arrow_array::BooleanArray::new_null(num_rows), } } -} -#[derive(Debug)] -pub struct TrackingDataContainer { - pub data: StructArray, - pub schema: Fields, - pub coordinate_system: Option, - pub orientation: Option, + pub fn pivot(data: StructArray) -> TrackingDataContainerResult { + use crate::datacontainer::builder::TrackingDataBuilder; + use ahash::AHashSet; + use arrow_array::types::{ + Float32Type, Float64Type, Int64Type, TimestampMicrosecondType, UInt64Type, + }; + use std::str::FromStr; + + if data.is_empty() { + return Ok(Self::new(data, Fields { fields: vec![] })); + } + + let fields = data.fields(); + let columns = data.columns(); + + // 1. Identify key columns + let mut frame_id_idx = None; + let mut timestamp_idx = None; + let mut player_id_idx = None; + let mut team_id_idx = None; + let mut attr_indices = Vec::new(); + let mut core_indices = Vec::new(); + + let core_names: AHashSet<&str> = [ + "match/id", + "competition/id", + "season/id", + "timestamp", + "frame_id", + "period", + "period_id", + "ball_status", + "ball_poss_team_id", + "ball_poss_status", + ] + .into_iter() + .collect(); + + for (i, field) in fields.iter().enumerate() { + let name = field.name().as_str(); + if name == "frame_id" { + frame_id_idx = Some(i); + core_indices.push(i); + } else if name == "timestamp" { + timestamp_idx = Some(i); + core_indices.push(i); + } else if name == "player/id" { + player_id_idx = Some(i); + } else if name == "team/id" { + team_id_idx = Some(i); + } else if core_names.contains(name) { + core_indices.push(i); + } else { + attr_indices.push(i); + } + } + + // 2. Select the best frame index column + let frame_idx_col = if let Some(fi) = frame_id_idx + && columns[fi].null_count() < columns[fi].len() + { + fi + } else if let Some(ti) = timestamp_idx { + ti + } else { + frame_id_idx.or(timestamp_idx).ok_or_else(|| { + TrackingDataContainerErrorKind::InvalidParameters( + "Missing frame_id or timestamp column".to_string(), + ) + })? + }; + + let frame_col = &columns[frame_idx_col]; + let is_frame_id = fields[frame_idx_col].name() == "frame_id"; + + let player_id_idx = player_id_idx.ok_or_else(|| { + TrackingDataContainerErrorKind::InvalidParameters( + "Missing player/id column".to_string(), + ) + })?; + let team_id_idx = team_id_idx.ok_or_else(|| { + TrackingDataContainerErrorKind::InvalidParameters("Missing team/id column".to_string()) + })?; + + // 3. Determine min/max frame index for fixed builder if possible + let mut min_f = i64::MAX; + let mut max_f = i64::MIN; + + let get_frame_val = |i: usize| -> Option { + if frame_col.is_null(i) { + return None; + } + match frame_col.data_type() { + Int64 => Some(frame_col.as_primitive::().value(i)), + UInt64 => Some(frame_col.as_primitive::().value(i) as i64), + Timestamp(_, _) => Some( + frame_col + .as_primitive::() + .value(i), + ), + _ => Some(i as i64), + } + }; + + for i in 0..data.len() { + if let Some(f) = get_frame_val(i) { + if f < min_f { + min_f = f; + } + if f > max_f { + max_f = f; + } + } + } + + // Only use fixed builder if it's a frame_id and the range is reasonable + let range = if min_f <= max_f { + (max_f - min_f) as usize + } else { + 0 + }; + let mut builder = if is_frame_id && min_f <= max_f && range < 1_000_000 { + TrackingDataBuilder::new_fixed(min_f, max_f) + } else { + TrackingDataBuilder::new() + }; + + // 4. Pre-create columns for core fields + struct CoreColumn { + idx: usize, + builder_idx: usize, + dtype: DataType, + } + let core_cols: Vec = core_indices + .iter() + .map(|&idx| { + let col_name = fields[idx].name(); + let dtype = fields[idx].data_type().clone(); + let builder_idx = builder.get_or_create_column_id(col_name, dtype.clone()); + CoreColumn { + idx, + builder_idx, + dtype, + } + }) + .collect(); + + // 5. Pre-calculate entity attribute mappings + let mut entity_map = ahash::AHashMap::new(); + + // 6. Pivot data + let mut last_frame_val = None; + let mut last_row_idx = 0; + + for i in 0..data.len() { + let f = match get_frame_val(i) { + Some(val) => val, + None => continue, + }; + + let row_idx = if Some(f) == last_frame_val { + last_row_idx + } else { + let ridx = builder.get_row_index(f); + last_frame_val = Some(f); + last_row_idx = ridx; + ridx + }; + + // Insert core columns (always check for NULLs, even if same frame) + for c in &core_cols { + let col = &columns[c.idx]; + if !col.is_null(i) { + match &c.dtype { + Int64 => builder.insert_int_at_idx( + row_idx, + c.builder_idx, + col.as_primitive::().value(i), + ), + UInt64 => builder.insert_int_at_idx( + row_idx, + c.builder_idx, + col.as_primitive::().value(i) as i64, + ), + Float32 => builder.insert_float_at_idx( + row_idx, + c.builder_idx, + col.as_primitive::().value(i) as f64, + ), + Float64 => builder.insert_float_at_idx( + row_idx, + c.builder_idx, + col.as_primitive::().value(i), + ), + Utf8 => builder.insert_string_at_idx( + row_idx, + c.builder_idx, + col.as_string::().value(i), + ), + LargeUtf8 => builder.insert_string_at_idx( + row_idx, + c.builder_idx, + col.as_string::().value(i), + ), + Timestamp(_, _) => builder.insert_int_at_idx( + row_idx, + c.builder_idx, + col.as_primitive::().value(i), + ), + _ => {} + } + } + } + + // Insert attribute columns + let player_col = &columns[player_id_idx]; + if player_col.is_null(i) { + continue; + } + let p_id = match player_col.data_type() { + Utf8 => player_col.as_string::().value(i), + LargeUtf8 => player_col.as_string::().value(i), + _ => continue, + }; + + let team_col = &columns[team_id_idx]; + let t_id = if team_col.is_null(i) { + "none" + } else { + match team_col.data_type() { + Utf8 => team_col.as_string::().value(i), + LargeUtf8 => team_col.as_string::().value(i), + _ => "none", + } + }; + + let attr_builder_indices = entity_map.entry((p_id, t_id)).or_insert_with(|| { + attr_indices + .iter() + .map(|&attr_idx| { + let attr_name = fields[attr_idx].name(); + let entity = if p_id == "ball" { + TrackedEntity::Ball + } else if let Some(team_id) = p_id.strip_prefix("team:") { + TrackedEntity::Team { + team_id: team_id.to_string(), + } + } else if let Some(referee_id) = p_id.strip_prefix("referee:") { + TrackedEntity::Referee { + referee_id: referee_id.to_string(), + } + } else { + TrackedEntity::Player { + team_id: t_id.to_string(), + player_id: p_id.to_string(), + } + }; + + let attribute = crate::schema::TrackedEntityAttribute::from_str(attr_name) + .unwrap_or_else(|_| { + crate::schema::TrackedEntityAttribute::Other(attr_name.to_string()) + }); + + let field = DataField::TrackedEntity { entity, attribute }; + builder.get_or_create_column_id( + &field.name(), + fields[attr_idx].data_type().clone(), + ) + }) + .collect::>() + }); + + for (attr_pos, &builder_col_idx) in attr_builder_indices.iter().enumerate() { + let attr_idx = attr_indices[attr_pos]; + let col = &columns[attr_idx]; + if !col.is_null(i) { + match col.data_type() { + Float32 => builder.insert_float_at_idx( + row_idx, + builder_col_idx, + col.as_primitive::().value(i) as f64, + ), + Float64 => builder.insert_float_at_idx( + row_idx, + builder_col_idx, + col.as_primitive::().value(i), + ), + Int64 => builder.insert_int_at_idx( + row_idx, + builder_col_idx, + col.as_primitive::().value(i), + ), + UInt64 => builder.insert_int_at_idx( + row_idx, + builder_col_idx, + col.as_primitive::().value(i) as i64, + ), + Utf8 => builder.insert_string_at_idx( + row_idx, + builder_col_idx, + col.as_string::().value(i), + ), + LargeUtf8 => builder.insert_string_at_idx( + row_idx, + builder_col_idx, + col.as_string::().value(i), + ), + _ => {} + } + } + } + } + + builder.build() + } } -impl Clone for TrackingDataContainer { - fn clone(&self) -> Self { - Self { - data: self.data.clone(), - schema: self.schema.clone(), + +impl crate::selector::Select for TrackingDataContainer { + fn select(&self, fields: Vec) -> TrackingDataContainerResult { + let indices = self.schema.get_column_indices(&fields); + + let mut new_arrays = Vec::with_capacity(fields.len()); + let mut new_arrow_fields = Vec::with_capacity(fields.len()); + let mut new_schema_fields = Vec::with_capacity(fields.len()); + + for (field, maybe_idx) in fields.into_iter().zip(indices.into_iter()) { + if let Some(idx) = maybe_idx { + new_arrays.push(self.data.column(idx).clone()); + new_arrow_fields.push(self.data.fields()[idx].clone()); + new_schema_fields.push(field); + } + } + + let new_data = if new_arrays.is_empty() { + StructArray::new_empty_fields(self.num_rows(), None) + } else { + StructArray::try_new(ArrowFields::from(new_arrow_fields), new_arrays, None)? + }; + + Ok(Self { + data: new_data, + schema: Fields { + fields: new_schema_fields, + }, coordinate_system: self.coordinate_system.clone(), orientation: self.orientation, - } + }) } } + impl crate::selector::expr::HasSchema for TrackingDataContainer { fn schema(&self) -> &Fields { &self.schema diff --git a/crates/tdc-core/src/schema/attribute.rs b/crates/tdc-core/src/schema/attribute.rs index f3fa88d..8317a54 100644 --- a/crates/tdc-core/src/schema/attribute.rs +++ b/crates/tdc-core/src/schema/attribute.rs @@ -168,8 +168,11 @@ impl Attribute for FrameAttribute { Period => DataType::Utf8, Timestamp => DataType::Timestamp(TimeUnit::Microsecond, None), BallStatus => DataType::Boolean, - MatchId | BallPossTeamId | BallPossStatus | VendorEventName | VendorTrackingName - | Other(_) => DataType::Utf8, + MatchId | BallPossTeamId | BallPossStatus | VendorEventName | VendorTrackingName => { + DataType::Utf8 + } + Other(name) if name == "x" || name == "y" || name == "z" => DataType::Float32, + Other(_) => DataType::Utf8, } } diff --git a/crates/tdc-python/src/container.rs b/crates/tdc-python/src/container.rs index 8ab2f72..93d9a29 100644 --- a/crates/tdc-python/src/container.rs +++ b/crates/tdc-python/src/container.rs @@ -134,6 +134,49 @@ impl PyTrackingDataContainer { }) } + #[staticmethod] + fn from_arrow( + _py: Python<'_>, + array_capsule: Bound<'_, PyCapsule>, + schema_capsule: Bound<'_, PyCapsule>, + coordinate_system: Option, + orientation: Option, + ) -> PyResult { + let array = pyo3_arrow::PyArray::from_arrow_pycapsule(&array_capsule, &schema_capsule)?; + let struct_array = array + .into_inner() + .0 + .as_any() + .downcast_ref::() + .ok_or_else(|| PyTypeError::new_err("Expected StructArray"))? + .clone(); + + let fields: Vec = struct_array + .fields() + .iter() + .map(|f| DataField::from_name(f.name())) + .collect(); + + let orientation = if let Some(o) = orientation { + use std::str::FromStr; + Some( + tdc_core::schema::OrientationMode::from_str(&o) + .map_err(|e| PyValueError::new_err(e.to_string()))?, + ) + } else { + None + }; + + Ok(PyTrackingDataContainer { + inner: TrackingDataContainer { + data: struct_array, + schema: Fields { fields }, + coordinate_system: coordinate_system.map(|cs| cs.inner), + orientation, + }, + }) + } + fn lazy(&self) -> PyResult { Ok(PyLazyTrackingDataContainer { inner: self.inner.lazy().into_pyresult()?, @@ -304,12 +347,27 @@ impl PyTrackingDataContainer { fn selector(&self) -> PyColumnSelector { PyColumnSelector } - fn __repr__(&self) -> String { - format!( - "", - self.inner.num_rows(), - self.inner.schema.len() - ) + + #[staticmethod] + fn pivot(data: Bound<'_, PyAny>) -> PyResult { + let capsules = data.call_method0("__arrow_c_array__")?; + let tuple = capsules.downcast::()?; + let array_item = tuple.get_item(0)?; + let array_capsule = array_item.downcast::()?; + let schema_item = tuple.get_item(1)?; + let schema_capsule = schema_item.downcast::()?; + + let array = pyo3_arrow::PyArray::from_arrow_pycapsule(array_capsule, schema_capsule)?; + let struct_array = array + .into_inner() + .0 + .as_any() + .downcast_ref::() + .ok_or_else(|| PyTypeError::new_err("Expected StructArray"))? + .clone(); + + let inner = TrackingDataContainer::pivot(struct_array).into_pyresult()?; + Ok(PyTrackingDataContainer { inner }) } fn materialize_frame<'py>(&self, py: Python<'py>, frame: usize) -> PyResult { diff --git a/crates/tdc-python/src/selector.rs b/crates/tdc-python/src/selector.rs index 37a64e5..42cc6bb 100644 --- a/crates/tdc-python/src/selector.rs +++ b/crates/tdc-python/src/selector.rs @@ -107,6 +107,19 @@ impl PyExpr { inner: self.inner.clone() / other.inner, } } + + fn __and__(&self, other: PyExpr) -> PyExpr { + PyExpr { + inner: self.inner.clone().and(other.inner), + } + } + + fn __or__(&self, other: PyExpr) -> PyExpr { + PyExpr { + inner: self.inner.clone().or(other.inner), + } + } + fn __pow__(&self, exponent: f64, _modulo: Option>) -> PyExpr { PyExpr { inner: power(self.inner.clone(), df::lit(exponent)), diff --git a/debug_warehouse/catalog.db b/debug_warehouse/catalog.db new file mode 100644 index 0000000000000000000000000000000000000000..36ea378b9bb8d1cf269b7325a0c2db34e631e5f1 GIT binary patch literal 20480 zcmeI&T~E_c7zglnybPAXywDIuOpPY6gu~9(y;U!C;u4JrIzh6l=CnJ7%2=W85N;Q) zjGqX<2^Yp2KLcKRwz*f`BqSJ!|7Och+ow6teouRGp6%V+72D^E<+!`dS5)bSB+JrM zMUf;aDBfe@-P!_TFxuJ?pK{lD&@d=1eY!an`5}#lPo?Q<~q+0J2b$gkv% z^c{Y5j;I4xuJU$VoKo65gTmh4K2 zAA0*`u~5`m)pr{E`Y;MTKJ4sRZCMV^M5FR?uBSAOopv#rdx1alH_x3Pyb<3ieH-ex z%5(w;$Lw;Sl|-HOvLhsIr_v5>xYrB3=kf#Fse1Z3VuzHblTG7}T~s;;M0p+u#%I=| ze=ASp60cK`=KhFyrY7po)ztL16bPS5;j`(_7kkcd6$Bsv0SG_<0uX=z1Rwx`D=ko~ z$P=@3^R>;|W{F#@TK0XHnLBo6tM#eZmw1+4=8s~r*oMblFD919hUg)1bKCb~&-no_ z*Bc12o-1_IsrbyUh~p)0RJZhx%;no+DahM#?LlLTSZj$G6N`2lCu+jTQZ=cPv}Twj znaEO-rL;ttK{KL-p_4WD~{&|v=7-?>)X-gyVq+yYy7!r+_62vqN%Sf

c5|Iv+YgAMh?rV&aL}byr7ICJM}Ju|G(pppXGxr< zHImFSN;E5@5^AMXHDe?#mR5Ve@#U0dSe8_jgPNMk4PuFVi6jlG5yrH%xR*+aQalq+ z#m!7_mLXA?K;%Sz{}fB*y_009U<00Izz00jO^fd^xvS0WE>lmzaJ znEA=Rn4W&sl(3T{-z4$E1_1~_00Izz00bZa0SG_<0uX?}H5Irgix$A`TYW76erS8X x=et#2|NUQ%e3!%v8w4N#0SG_<0uX=z1Rwwb2tWV=*ID3By$8_$H-Kgj;3q2ZSfKy_ literal 0 HcmV?d00001 diff --git a/debug_warehouse/tracking/match_metadata/metadata/00000-140c93eb-f000-4b09-aa96-69d2d17151c7.metadata.json b/debug_warehouse/tracking/match_metadata/metadata/00000-140c93eb-f000-4b09-aa96-69d2d17151c7.metadata.json new file mode 100644 index 0000000..55ab7ac --- /dev/null +++ b/debug_warehouse/tracking/match_metadata/metadata/00000-140c93eb-f000-4b09-aa96-69d2d17151c7.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/debug_warehouse/tracking/match_metadata","table-uuid":"9b026c5b-217b-4c37-912c-4ccc1cd7a84f","last-updated-ms":1775575511744,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":true},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"snapshots":[],"snapshot-log":[],"metadata-log":[],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":0} diff --git a/debug_warehouse/tracking/match_metadata/metadata/00001-702060e8-1069-48a0-9f73-0f5337b4fa63.metadata.json b/debug_warehouse/tracking/match_metadata/metadata/00001-702060e8-1069-48a0-9f73-0f5337b4fa63.metadata.json new file mode 100644 index 0000000..cbd84b5 --- /dev/null +++ b/debug_warehouse/tracking/match_metadata/metadata/00001-702060e8-1069-48a0-9f73-0f5337b4fa63.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/debug_warehouse/tracking/match_metadata","table-uuid":"9b026c5b-217b-4c37-912c-4ccc1cd7a84f","last-updated-ms":1775575511895,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":true},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"current-snapshot-id":8457677481870634211,"snapshots":[{"snapshot-id":8457677481870634211,"sequence-number":1,"timestamp-ms":1775575511895,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/debug_warehouse/tracking/match_metadata/metadata/snap-8457677481870634211-0-4f80fb8d-8d7d-435b-95e1-55f2b9ea7dd3.avro","summary":{"operation":"append","added-files-size":"1551","added-data-files":"1","added-records":"1","total-data-files":"1","total-delete-files":"0","total-records":"1","total-files-size":"1551","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0}],"snapshot-log":[{"snapshot-id":8457677481870634211,"timestamp-ms":1775575511895}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/debug_warehouse/tracking/match_metadata/metadata/00000-140c93eb-f000-4b09-aa96-69d2d17151c7.metadata.json","timestamp-ms":1775575511744}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":8457677481870634211,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":1} diff --git a/debug_warehouse/tracking/match_metadata/metadata/4f80fb8d-8d7d-435b-95e1-55f2b9ea7dd3-m0.avro b/debug_warehouse/tracking/match_metadata/metadata/4f80fb8d-8d7d-435b-95e1-55f2b9ea7dd3-m0.avro new file mode 100644 index 0000000000000000000000000000000000000000..0c927794bb8e6faabad7e9dd3e1e951d386b27e7 GIT binary patch literal 4474 zcmb`K-)kII6vx{b(l(|E5-Ey7u0vusu-oj+Y<}#vHW5rPZLPsZ3=PY0cjj(0&FsuL zb0;B|AVQ@G5;TZHT9T@T_N^_L`XVHVf%c&f)|Ln&`WN^v_~5yBGP5(gX?}Gfgk)y! z_niAVKkm8j7B22@ZgSNLQo{e$UlQnyO@y4_QpZ!NkQ9nKF*H}mjb9RUO~{E!!Neu@ zMF~@Nq6?lSBWrZ?0{rig4?W!>@HwcE*`&Wz3YV%@$tF~%x@B%BP{f8CD#)RNOR&4k z#hGw%iBPOzij|9?vAz5Gq~PO8F#zc#a3Cht0gRL4bl^Dyl%H+0vEy$m<=RB8AI}T% zC-7xmx~*tA053H~92W@GNiX6l$2z838c~}yQZz6n&8lTmuyT<3d4#|BeV98Qga`p< z)pCG83WJ0oN`80{L=aQY4c~I-Q5rt>Ba8&yq{3_xMOK*Db|);Vu%K>;`pd-fvT?!m z41>KlEC_ZU{s5n`XHX%B*sHiCWCAf&qL^N(K%5F$d9SiX-5`p)rFLeo+B`FufeL4( zvfxfbuo1)xAyJO_#-qR(S}O1phQKcJ^8>L!<`9d05?PMQej!LF09p|@PQ%7@qS8=0 z8g8g)UY((>GnR^338MGk8%EPQoj^n95YI9a&kL^aJ5^;Nk{PUL#4YS;H(|w8+1hag zSW*UM;D~97Z6_c+5|k)nfkxD`4U+V?jZp!+L_>gtP7uc0FJ{DwIgZ3HJw>%VC?XIK z`T)dGOh?k%FOstLv%CzDRYbmmi*(-@V;ncZsk{0|MA1#9FhfaYEsN=B9`R}{8ydAJ zHW1T5OoN&6HDcwdT839L74}87dLO85^op?P%bAZI2hXsr>y(tB{XriI8cVq^1woh; z#a?Af6#GI}Y(=D5O%kpZD6rN9(ow2nZ_M*4YFym7GyeigohZ5AS*4$?J5E%;$TIZMb7O?M~b;o=VY2~mwQ3)&lw)&OM| zP)g|#(C$%YcaKs^*BT|Sog^ftYIqv-4SskaY-;E%J{xDtjumAqz+$A)Jv(4YWPqi; z1D3=+0G4V8nDIiK7vwPOP%(E*Y;Flt?yY6E%6$M+&h8zi+!ynHM};qEYYCH0F03Cw zUiwtSuI+b-I9|h=CXP}7?l2Vs$!J58x@(D6>S1}>8yeYlBanKwM!V^$BlXsfEoX%| zt2ji@wx15MrU#S6x~cpO-BjA9n{EnGu3_ehX$* z+aWHy^ujUif(f{afC+X{fk|0+xhwui6-;~%4Bb37!GjVu^&)Yp0y55wFuU)iw(EzR z51&4ze)r8n;{Hgw?x*L}LhjzbfB&=chxzBnYxh@I?_B9j$w!}@Qk4E5`j&5vtz1v^ zcXlo(y0hQk82hVZIoW-51ueh5aw8#TkIP+eob0&UE}qQx-)mc-)x!sx=Q%Hr5p z3tuYduQh&fbLr~X{IwUBuAZMi_~_e~*P8|g)(6(xAO4I|4}`&WVeQ1=`Y*p{hSwiu zo{X$N&OA&!kh*_8(Xlpm8{NHiC$oIHYgJ67A9r8Y7wcOO{FWQ~y76A)+@a5#2AW%+ rZ++#9I`QSzGYt*s%BP>T6n=c|z-celu=L5H8*}^jy?NwJ<%a(Uw&;i< literal 0 HcmV?d00001 diff --git a/debug_warehouse/tracking/match_metadata/metadata/snap-8457677481870634211-0-4f80fb8d-8d7d-435b-95e1-55f2b9ea7dd3.avro b/debug_warehouse/tracking/match_metadata/metadata/snap-8457677481870634211-0-4f80fb8d-8d7d-435b-95e1-55f2b9ea7dd3.avro new file mode 100644 index 0000000000000000000000000000000000000000..4f9ad53cc3f469dab842daf6236b02f84598007c GIT binary patch literal 1819 zcmb7ET}TvB7`5#oqgo7qk&~;<4P{m#COJ^46JVRG{R36`)E{XkcCgI9?1Q36rp(&qq)ekAZbbjhP`L5=`PYBmhjZ z@pK$J~ET)8JV`LxyT>2^E%7krB~YfF>qk$<*Nu4GKKT%qqpo(of+f z6)%JfNX5kD#UM811hN{y=Nn)H2Pv>UV?`pl7FxI*Lz5!|5Jf2R)?6%AbK@%TQAvgd zFQA0Id|!aV+dPA3BmVz92lG} zv?_2QpgpvwP@G$1@%Z?t+3RG?jVBTF3k!4|q)ZI)P%-aAVHr8{Bw?tS8$2KFyx!*Y z14%W=80J*+H^5@tO$|W=AsGVIzC3w2`60o%3sOL{W# literal 0 HcmV?d00001 diff --git a/debug_warehouse/tracking/tracking_data/metadata/00000-b6ef36f9-14bf-436f-9ed2-cbbfb2a553b8.metadata.json b/debug_warehouse/tracking/tracking_data/metadata/00000-b6ef36f9-14bf-436f-9ed2-cbbfb2a553b8.metadata.json new file mode 100644 index 0000000..9396968 --- /dev/null +++ b/debug_warehouse/tracking/tracking_data/metadata/00000-b6ef36f9-14bf-436f-9ed2-cbbfb2a553b8.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/debug_warehouse/tracking/tracking_data","table-uuid":"5f22a43f-a3e8-4c88-8ce9-5abed223a490","last-updated-ms":1775575511736,"last-column-id":11,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[{"source-id":1,"field-id":1000,"transform":"identity","name":"match/id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"snapshots":[],"snapshot-log":[],"metadata-log":[],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":0} diff --git a/debug_warehouse/tracking/tracking_data/metadata/00001-32b80343-69bc-4280-800d-ab07ceb3c869.metadata.json b/debug_warehouse/tracking/tracking_data/metadata/00001-32b80343-69bc-4280-800d-ab07ceb3c869.metadata.json new file mode 100644 index 0000000..cb0921d --- /dev/null +++ b/debug_warehouse/tracking/tracking_data/metadata/00001-32b80343-69bc-4280-800d-ab07ceb3c869.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/debug_warehouse/tracking/tracking_data","table-uuid":"5f22a43f-a3e8-4c88-8ce9-5abed223a490","last-updated-ms":1775575511888,"last-column-id":14,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false},{"id":12,"name":"player_id","type":"string","required":false},{"id":13,"name":"team_id","type":"string","required":false},{"id":14,"name":"entity_key","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":1,"field-id":1000,"transform":"identity","name":"match/id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":6099500306663132840,"snapshots":[{"snapshot-id":6099500306663132840,"sequence-number":1,"timestamp-ms":1775575511888,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/debug_warehouse/tracking/tracking_data/metadata/snap-6099500306663132840-0-e525fa0c-bd68-4ba7-bfc2-9a6c8f662a6d.avro","summary":{"operation":"append","added-files-size":"4767","added-data-files":"1","added-records":"2","changed-partition-count":"1","total-data-files":"1","total-delete-files":"0","total-records":"2","total-files-size":"4767","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":6099500306663132840,"timestamp-ms":1775575511888}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/debug_warehouse/tracking/tracking_data/metadata/00000-b6ef36f9-14bf-436f-9ed2-cbbfb2a553b8.metadata.json","timestamp-ms":1775575511736}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":6099500306663132840,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":1} diff --git a/debug_warehouse/tracking/tracking_data/metadata/e525fa0c-bd68-4ba7-bfc2-9a6c8f662a6d-m0.avro b/debug_warehouse/tracking/tracking_data/metadata/e525fa0c-bd68-4ba7-bfc2-9a6c8f662a6d-m0.avro new file mode 100644 index 0000000000000000000000000000000000000000..c5d44b17b6be17c165f16c0eb14a5fb1267b6de4 GIT binary patch literal 5356 zcmb_gYitx%6o#UaASsG1@j>X#V2qG$*`4iMED^L?lu}EH)Mj08XYbOn^J3<1+bvxa zsv$sxm{71pF+xR&;fJq~G$@KcM%`M9V^q=>;)Hm&8v2v-^KaSE2% z8#1Os%K~`jOR^D_)$qSg*2F}e!0S;g86@GeY6bY6Q5_%%Cm&r>$2xeaW2q5cR5O6A%(awooah;$Rqhy5g5``% zD;(1f7o(`8Vn&Hk@Co=GqxkrZTtan@-^gW>X2RmB1yFZP8U|bzFiP;*~t40m@W|8)P5hD z(eDS$sg4yRs_HT{rRA2glv3L?5#qeA!C#JlHZ#Puu#M;jbYl*mhud_uNKhk0$c>Pw zgel1tRE0viQzMw-S?1@~VQwz8^#TDY1XTxSBS$e1WNsyaKu$1?8P2EXQIH>Aw&VtJ zqC&ZZ4<%VBSTkBwYL;i%sbv;$&6+qRCP~b#*?=MV19HYOiE@>QedKisvP2Ytgq4`w zO!Sm&f)jNc6(tfj#`Il0QQr*lFbj~9EblrTZwZErnBpi9Ov=ry0>e-h06(r8;v$P5 zh!wIDvD*8PstfEF0y+VpDRnc^&@gRD4Fptm%WXG`6YCjOz-%0#mGw2KU8F6jdId6B zM!dW*{aZ<;MMz=>8(^jiI1wgv8mD8@Vojg|=EMwl@YQT`cqz>q*+Iu;_3$AWuSJ0X zXIBrah22-~5Kd!0GqPXP1Ad;tXjit*))>SHhGqh@T72!c}37Q$+5sS#po ze&BwE2(ZM`APY8zrFOj{+~9H%5z zLjwL}{#s+$Ic9@Ov4ofp#C)(YmJg3C2&xp5l`wlF-FrEtJ*-hqS=^4px{g6>p*obv zpx>ZhwxGFsXcwV15GV_`1p?&`R?H$vB+XIcgtMD58P!0UC-XKe#Y|bWd%UV`)>itip-(p{n>`n>>VEvQ;=d2&?|FjA*@7DEw1%q%f?xFEP!GE z$|AsgfC0=;9AG{$0f71J0nG41CSJf{rcj=A6s(L1X0R-qUsrvL9FD z2P?7(Cfk^p9sn;bR2|i}6v7Lyq_z`1+zhzGS_?=f3ld+FO=$U2#?vw;K43t~)z>*8o2W_2*}OihEgpr-u)sOg9x2D2!qCRQVX2tze26N=fQ ztHA`x9AUh8a;o;(f;f*+4w9+#iXRD@U zSejXe)$A)K*f0zfW1A=J84*H>E{h65y4sHH4B7tX{2hUq6s{+*+!;kFpsPAyu1BDG z(l_nmk*1`%YmHU$n#W;R)pTO8TRV(t146(R2tu$65^Tz%VWwiORQATF!_sZm#(p4U zMU28%PhoJQ$GOC$g}=96IN&S$taqERp;rBCbI$hL<;`C#{kkr1;QP;KFMWOHlBs)o z_H7B241GH7bipr&YQ8ys>Y;wITdd#O5G=nsuWQP#gNyGt|0mbIwJ)!0a>1bopW1c$ zzN)I@mv0>3bv1GAqmIOl&Mt1!hwr>_*OgG{*BkSe*C%=o@Bi+eOV=AeDEOyo&cNKa z&ozGa)&+F`s-mO4XAVC*(7ku)z;APA?0aOpwrI}#T|1fz&uyFDc=q}O1L7Z-Rt}!{ z>ExLfd2{<~TaKLA5YKmw?qjX}e=i%n(pglyvFE6<`&i9fwd(c3p1lih+jK8CSodYgzP=4Jzuo)d zlsAVK=RZ9?fBm#2>mFN|v*gZ(Nz)phugZV-mA=@+Pv%Lp)_+_#w6R{?+Sb%vFr$C| j8|SJv%{_QxXV=Y(wb!;S{r6(do-?Nk-dnRK72W>;u6fjc literal 0 HcmV?d00001 diff --git a/debug_warehouse/tracking/tracking_data/metadata/snap-6099500306663132840-0-e525fa0c-bd68-4ba7-bfc2-9a6c8f662a6d.avro b/debug_warehouse/tracking/tracking_data/metadata/snap-6099500306663132840-0-e525fa0c-bd68-4ba7-bfc2-9a6c8f662a6d.avro new file mode 100644 index 0000000000000000000000000000000000000000..b07c799e0766df7977b9f9b082d5ee09dc35fb6a GIT binary patch literal 1829 zcmb7ET}TvB7_BW~Gy9V;DV1p`v2tU^)orz)h%yW@wrQ8-B7*Hb~TbNr5D@p-|r^8Sap+^g=0cg1R zydW}EKs?RRz%tTYCF?=6fp&^Wg5qik1{FYHPW%arA*g~I8(LWs)gfEKum%*7A+f~_ z8A<+N_xXcE2W~|CI*JKM9V(_J90FneH!J3QtYWDsEJ}4^?3nB_T z%jZgMc!y^&B4a|EEu>?xd*EP@paKakAsvC;0tZ3D0Ry!xPylaly0ZnT3eqs7x6rB} z4T0T5X9}sg#dF7pv&~*TYi>M^m|0k07$9n5sE4X~ACk-Hr%y70s=2{4(dpOQoPHo{ zn2upwC2<2R#&X*b1Q1dn(45PYfYTou9KRq*G;ijUJPxTUSc|5Ce$vAP!V013n(DKb zT0t$)5CU{G9qY{7j*g8tN+4zzVGZm0f~>Y9^AD`1kaZ%BGS1J&VvA@u!Kk1@0wh3y z!z!E6|0$lj&*mnvu_cdlr!zLi2Ck0})$Jb~eOKUm=Bho#-F5f#pV6kQuNl=-_xBB6dd@<;Vr5}x z$3*to!<}n+_mdBy%;BM~RhxTWKK=Fn?eB`bLp|N!9(IEZ8{4\n", + "shape: (5, 6)
match/idtimestampframe_idperiodball/xball/y
strdatetime[μs]u64strf32f32
"match_A"1969-12-31 22:00:01nullnull0.00.0
"match_A"1969-12-31 22:00:01.040nullnull1.00.5
"match_A"1969-12-31 22:00:01.080nullnull2.01.0
"match_A"1969-12-31 22:00:01.120nullnull3.01.5
"match_A"1969-12-31 22:00:01.160nullnull4.02.0
" + ], + "text/plain": [ + "shape: (5, 6)\n", + "┌──────────┬─────────────────────────┬──────────┬────────┬────────┬────────┐\n", + "│ match/id ┆ timestamp ┆ frame_id ┆ period ┆ ball/x ┆ ball/y │\n", + "│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │\n", + "│ str ┆ datetime[μs] ┆ u64 ┆ str ┆ f32 ┆ f32 │\n", + "╞══════════╪═════════════════════════╪══════════╪════════╪════════╪════════╡\n", + "│ match_A ┆ 1969-12-31 22:00:01 ┆ null ┆ null ┆ 0.0 ┆ 0.0 │\n", + "│ match_A ┆ 1969-12-31 22:00:01.040 ┆ null ┆ null ┆ 1.0 ┆ 0.5 │\n", + "│ match_A ┆ 1969-12-31 22:00:01.080 ┆ null ┆ null ┆ 2.0 ┆ 1.0 │\n", + "│ match_A ┆ 1969-12-31 22:00:01.120 ┆ null ┆ null ┆ 3.0 ┆ 1.5 │\n", + "│ match_A ┆ 1969-12-31 22:00:01.160 ┆ null ┆ null ┆ 4.0 ┆ 2.0 │\n", + "└──────────┴─────────────────────────┴──────────┴────────┴────────┴────────┘" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Load specific match (returns a Lazy container)\n", + "lazy_loaded = catalog.load_container(match_id=\"match_A\")\n", + "\n", + "print(f\"Is lazy: {isinstance(lazy_loaded, tdc.LazyTrackingDataContainer)}\")\n", + "\n", + "# Execute the plan and pivot to wide format\n", + "loaded = lazy_loaded.collect()\n", + "\n", + "print(f\"Loaded container bound: {loaded.is_bound()}\")\n", + "print(f\"Rows: {loaded.num_rows()}\")\n", + "loaded.to_polars().head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Modify and Save (Catalog Binding)\n", + "\n", + "A container loaded from a catalog is \"bound\" to it. This means we can perform transformations and then simply call `.save()` to commit the changes back to the warehouse." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Modified container bound: True\n", + "Changes committed to catalog.\n" + ] + } + ], + "source": [ + "# Add a new column (Schema Evolution)\n", + "# Note: ball/z is a standardized CDF field that maps to Float32\n", + "modified = loaded.add_column(\"ball/z\", [0.5] * loaded.num_rows())\n", + "\n", + "print(f\"Modified container bound: {modified.is_bound()}\")\n", + "\n", + "# Atomic commit back to Iceberg\n", + "modified.save()\n", + "print(\"Changes committed to catalog.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Verify Schema Evolution\n", + "\n", + "We can now load the data again and see that the new column is present." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "

\n", + "shape: (5, 7)
match/idtimestampframe_idperiodball/xball/yball/z
strdatetime[μs]u64strf32f32f32
"match_A"1969-12-31 20:00:01nullnull0.00.00.5
"match_A"1969-12-31 20:00:01.040nullnull1.00.50.5
"match_A"1969-12-31 20:00:01.080nullnull2.01.00.5
"match_A"1969-12-31 20:00:01.120nullnull3.01.50.5
"match_A"1969-12-31 20:00:01.160nullnull4.02.00.5
" + ], + "text/plain": [ + "shape: (5, 7)\n", + "┌──────────┬─────────────────────────┬──────────┬────────┬────────┬────────┬────────┐\n", + "│ match/id ┆ timestamp ┆ frame_id ┆ period ┆ ball/x ┆ ball/y ┆ ball/z │\n", + "│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │\n", + "│ str ┆ datetime[μs] ┆ u64 ┆ str ┆ f32 ┆ f32 ┆ f32 │\n", + "╞══════════╪═════════════════════════╪══════════╪════════╪════════╪════════╪════════╡\n", + "│ match_A ┆ 1969-12-31 20:00:01 ┆ null ┆ null ┆ 0.0 ┆ 0.0 ┆ 0.5 │\n", + "│ match_A ┆ 1969-12-31 20:00:01.040 ┆ null ┆ null ┆ 1.0 ┆ 0.5 ┆ 0.5 │\n", + "│ match_A ┆ 1969-12-31 20:00:01.080 ┆ null ┆ null ┆ 2.0 ┆ 1.0 ┆ 0.5 │\n", + "│ match_A ┆ 1969-12-31 20:00:01.120 ┆ null ┆ null ┆ 3.0 ┆ 1.5 ┆ 0.5 │\n", + "│ match_A ┆ 1969-12-31 20:00:01.160 ┆ null ┆ null ┆ 4.0 ┆ 2.0 ┆ 0.5 │\n", + "└──────────┴─────────────────────────┴──────────┴────────┴────────┴────────┴────────┘" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "final = catalog.load_container(match_id=\"match_A\").collect()\n", + "final.to_polars().head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Cleanup\n", + "\n", + "Remove the example warehouse." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Warehouse cleaned up.\n" + ] + } + ], + "source": [ + "if Path(warehouse_path).exists():\n", + " shutil.rmtree(warehouse_path)\n", + "print(\"Warehouse cleaned up.\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "/Users/probberechts/Developer/tracking_data_container", + "language": "python", + "name": "tracking_data_container" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.14" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/08-idsse-warehouse.ipynb b/examples/08-idsse-warehouse.ipynb new file mode 100644 index 0000000..477b6a2 --- /dev/null +++ b/examples/08-idsse-warehouse.ipynb @@ -0,0 +1,376 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Multi-Match Warehouse with IDSSE Dataset\n", + "\n", + "This notebook demonstrates building a multi-match tracking data warehouse using the **IDSSE dataset**. \n", + "\n", + "We will:\n", + "1. Discover multiple matches in the local `data` directory.\n", + "2. Parse the Sportec (DFL) XML files using TDC's high-performance Rust parser.\n", + "3. Ingest all matches into a single Iceberg table using a scalable hybrid format.\n", + "4. Demonstrate efficient cross-match lazy querying." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import tracking_data_container as tdc\n", + "import pyarrow as pa\n", + "import polars as pl\n", + "from pathlib import Path\n", + "import shutil\n", + "import time\n", + "import os" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Discover Data\n", + "\n", + "We use the `examples/data` directory which contains professional matches in Sportec format." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Found 7 matches to ingest.\n", + " - DFL-MAT-J03WMX\n", + " - DFL-MAT-J03WN1\n", + " - DFL-MAT-J03WOH\n", + " - DFL-MAT-J03WOY\n", + " - DFL-MAT-J03WPY\n", + " - DFL-MAT-J03WQQ\n", + " - DFL-MAT-J03WR9\n" + ] + } + ], + "source": [ + "data_dir = Path(\"data\")\n", + "if not data_dir.exists():\n", + " # Fallback for if we are running from the root instead of examples/\n", + " data_dir = Path(\"examples/data\")\n", + "\n", + "all_files = os.listdir(data_dir)\n", + "\n", + "# Pair position files with metadata files\n", + "pos_files = sorted([f for f in all_files if \"positions_raw_observed\" in f])\n", + "meta_files = sorted([f for f in all_files if \"matchinformation\" in f])\n", + "\n", + "pairs = []\n", + "for pos in pos_files:\n", + " # Extract match ID (the last part of the filename)\n", + " match_id = pos.split(\"_\")[-1].replace(\".xml\", \"\")\n", + " meta = next((m for m in meta_files if match_id in m), None)\n", + " if meta:\n", + " pairs.append({\"match_id\": match_id, \"pos\": pos, \"meta\": meta})\n", + "\n", + "print(f\"Found {len(pairs)} matches to ingest.\")\n", + "for p in pairs:\n", + " print(f\" - {p['match_id']}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Setup Warehouse\n", + "\n", + "We initialize our `TrackingDataCatalog`." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Warehouse initialized at ./idsse_warehouse\n" + ] + } + ], + "source": [ + "warehouse_path = \"./idsse_warehouse\"\n", + "if Path(warehouse_path).exists():\n", + " shutil.rmtree(warehouse_path)\n", + "\n", + "catalog = tdc.TrackingDataCatalog(warehouse_path=warehouse_path)\n", + "catalog.initialize()\n", + "print(f\"Warehouse initialized at {warehouse_path}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Ingest Matches\n", + "\n", + "For each match, we parse the raw XML using the optimized Rust backend and save it to the Iceberg table. \n", + "\n", + "**Hybrid Approach**: The container is wide-format (one column per player), but the catalog automatically \"melts\" it into a fixed long-format schema for storage." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Processing match DFL-MAT-J03WMX...\n", + " ✅ Parsed in 1.76s, Saved in 2.21s\n", + " ✅ Rows: 145,967\n", + "\n", + "Processing match DFL-MAT-J03WN1...\n", + " ✅ Parsed in 1.91s, Saved in 1.93s\n", + " ✅ Rows: 141,561\n", + "\n", + "Processing match DFL-MAT-J03WOH...\n", + " ✅ Parsed in 1.30s, Saved in 1.73s\n", + " ✅ Rows: 137,214\n" + ] + } + ], + "source": [ + "for pair in pairs[:3]: # Let's just do 3 for speed in this example\n", + " match_id = pair['match_id']\n", + " print(f\"\\nProcessing match {match_id}...\")\n", + " \n", + " pos_path = data_dir / pair['pos']\n", + " meta_path = data_dir / pair['meta']\n", + " \n", + " # Parse (High-Performance Rust)\n", + " start = time.perf_counter()\n", + " container = tdc.load_sportec(str(pos_path), str(meta_path))\n", + " parse_time = time.perf_counter() - start\n", + " \n", + " # Add match ID column (required for partitioning)\n", + " container = container.add_column(\"match/id\", [match_id] * container.num_rows())\n", + " \n", + " # Save to Warehouse (melts to long format internally)\n", + " start = time.perf_counter()\n", + " catalog.save_container(container)\n", + " save_time = time.perf_counter() - start\n", + " \n", + " print(f\" ✅ Parsed in {parse_time:.2f}s, Saved in {save_time:.2f}s\")\n", + " print(f\" ✅ Rows: {container.num_rows():,}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Querying the Warehouse\n", + "\n", + "Now that our data is stored in Iceberg, we can perform efficient queries. \n", + "\n", + "### Count rows per match\n", + "Loading is instantaneous because it is **Lazy**. The data is only pivoted back to the wide format when you call `.collect()`." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Warehouse Summary:\n", + " - Match DFL-MAT-J03WMX: 165,259 rows\n", + " - Match DFL-MAT-J03WN1: 162,273 rows\n", + " - Match DFL-MAT-J03WOH: 157,790 rows\n", + "\n", + "Total rows in warehouse: 485,322\n" + ] + } + ], + "source": [ + "print(\"Warehouse Summary:\")\n", + "total_rows = 0\n", + "for pair in pairs[:3]:\n", + " m_id = pair['match_id']\n", + " # returns a LazyTrackingDataContainer\n", + " lazy_container = catalog.load_container(match_id=m_id)\n", + " \n", + " # Execute plan and pivot to wide format\n", + " loaded = lazy_container.collect()\n", + " \n", + " rows = loaded.num_rows()\n", + " total_rows += rows\n", + " print(f\" - Match {m_id}: {rows:,} rows\")\n", + "\n", + "print(f\"\\nTotal rows in warehouse: {total_rows:,}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load specific player\n", + "We can load data for a specific match and see the wide-format columns automatically reconstructed." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading data for DFL-MAT-J03WMX...\n", + " ✅ Loaded and pivoted 165259 rows in 1.3019s\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "shape: (5, 348)
match/idcompetition/idseason/idtimestampframe_idperiodteams/home/players/DFL-OBJ-0002HE/xteams/home/players/DFL-OBJ-0002HE/yteams/home/players/DFL-OBJ-0002HE/zteams/home/players/DFL-OBJ-0002HE/velteams/home/players/DFL-OBJ-0002HE/accteams/home/players/DFL-OBJ-0002HE/entity_keyteams/home/players/DFL-OBJ-0002HE/idteams/home/players/DFL-OBJ-0002HE/jersey_colourteams/home/players/DFL-OBJ-0002HE/nameteams/home/xteams/home/yteams/home/zteams/home/velteams/home/accteams/home/entity_keyteams/home/idteams/home/jersey_colourteams/home/nameteams/away/players/DFL-OBJ-0002F5/xteams/away/players/DFL-OBJ-0002F5/yteams/away/players/DFL-OBJ-0002F5/zteams/away/players/DFL-OBJ-0002F5/velteams/away/players/DFL-OBJ-0002F5/accteams/away/players/DFL-OBJ-0002F5/entity_keyteams/away/players/DFL-OBJ-0002F5/idteams/away/players/DFL-OBJ-0002F5/jersey_colourteams/away/players/DFL-OBJ-0002F5/nameteams/away/players/DFL-OBJ-0002AU/xteams/away/players/DFL-OBJ-0002AU/yteams/away/players/DFL-OBJ-0002AU/zteams/away/players/DFL-OBJ-0002AU/velteams/away/nameteams/away/players/DFL-OBJ-J017RE/xteams/away/players/DFL-OBJ-J017RE/yteams/away/players/DFL-OBJ-J017RE/zteams/away/players/DFL-OBJ-J017RE/velteams/away/players/DFL-OBJ-J017RE/accteams/away/players/DFL-OBJ-J017RE/entity_keyteams/away/players/DFL-OBJ-J017RE/idteams/away/players/DFL-OBJ-J017RE/jersey_colourteams/away/players/DFL-OBJ-J017RE/nameteams/away/players/DFL-OBJ-000191/xteams/away/players/DFL-OBJ-000191/yteams/away/players/DFL-OBJ-000191/zteams/away/players/DFL-OBJ-000191/velteams/away/players/DFL-OBJ-000191/accteams/away/players/DFL-OBJ-000191/entity_keyteams/away/players/DFL-OBJ-000191/idteams/away/players/DFL-OBJ-000191/jersey_colourteams/away/players/DFL-OBJ-000191/nameteams/away/players/DFL-OBJ-0027KL/xteams/away/players/DFL-OBJ-0027KL/yteams/away/players/DFL-OBJ-0027KL/zteams/away/players/DFL-OBJ-0027KL/velteams/away/players/DFL-OBJ-0027KL/accteams/away/players/DFL-OBJ-0027KL/entity_keyteams/away/players/DFL-OBJ-0027KL/idteams/away/players/DFL-OBJ-0027KL/jersey_colourteams/away/players/DFL-OBJ-0027KL/nameteams/away/players/DFL-OBJ-0000IA/xteams/away/players/DFL-OBJ-0000IA/yteams/away/players/DFL-OBJ-0000IA/zteams/away/players/DFL-OBJ-0000IA/velteams/away/players/DFL-OBJ-0000IA/accteams/away/players/DFL-OBJ-0000IA/entity_keyteams/away/players/DFL-OBJ-0000IA/idteams/away/players/DFL-OBJ-0000IA/jersey_colourteams/away/players/DFL-OBJ-0000IA/name
strstrstrdatetime[μs]i64strf32f32f32f64f64strstrstrstrf32f32f32f64f64strstrstrstrf32f32f32f64f64strstrstrstrf32f32f32f64strf32f32f32f64f64strstrstrstrf32f32f32f64f64strstrstrstrf32f32f32f64f64strstrstrstrf32f32f32f64f64strstrstrstr
"DFL-MAT-J03WMX"nullnull2023-05-27 13:30:12.60010000"first_half"47.3699990.25null0.00.0"DFL-OBJ-0002HE|home"nullnullnullnullnullnullnullnull"team:home|home""DFL-CLU-000008""#F4F5F0""1. FC Köln"-9.09-1.96null0.00.0"DFL-OBJ-0002F5|away"nullnullnull0.11-21.870001null0.0"FC Bayern München"-20.779.07null0.00.0"DFL-OBJ-J017RE|away"nullnullnullnullnullnullnullnullnullnullnullnull-16.0499991.04null0.00.0"DFL-OBJ-0027KL|away"nullnullnull-1.5312.68null0.00.0"DFL-OBJ-0000IA|away"nullnullnull
"DFL-MAT-J03WMX"nullnull2023-05-27 13:30:12.64010001"first_half"47.340.26null3.560.42"DFL-OBJ-0002HE|home"nullnullnullnullnullnullnullnull"team:home|home""DFL-CLU-000008""#F4F5F0""1. FC Köln"-9.06-1.95null3.10.0"DFL-OBJ-0002F5|away"nullnullnull0.08-21.870001null1.73"FC Bayern München"-20.779.11null3.12.11"DFL-OBJ-J017RE|away"nullnullnullnullnullnullnullnullnullnullnullnull-15.980.99null7.270.66"DFL-OBJ-0027KL|away"nullnullnull-1.4512.68null11.340.0"DFL-OBJ-0000IA|away"nullnullnull
"DFL-MAT-J03WMX"nullnull2023-05-27 13:30:12.68010002"first_half"47.2999990.26null3.620.37"DFL-OBJ-0002HE|home"nullnullnullnullnullnullnullnull"team:home|home""DFL-CLU-000008""#F4F5F0""1. FC Köln"-9.02-1.94null3.170.0"DFL-OBJ-0002F5|away"nullnullnull0.04-21.860001null2.03"FC Bayern München"-20.779.14null3.42.06"DFL-OBJ-J017RE|away"nullnullnullnullnullnullnullnullnullnullnullnull-15.920.94null7.32-0.04"DFL-OBJ-0027KL|away"nullnullnull-1.2712.65null12.80.0"DFL-OBJ-0000IA|away"nullnullnull
"DFL-MAT-J03WMX"nullnull2023-05-27 13:30:12.72010003"first_half"47.2599980.26null3.680.34"DFL-OBJ-0002HE|home"nullnullnullnullnullnullnullnull"team:home|home""DFL-CLU-000008""#F4F5F0""1. FC Köln"-8.99-1.93null3.290.0"DFL-OBJ-0002F5|away"nullnullnull0.01-21.85null2.57"FC Bayern München"-20.779.19null3.72.0"DFL-OBJ-J017RE|away"nullnullnullnullnullnullnullnullnullnullnullnull-15.860.89null7.26-0.74"DFL-OBJ-0027KL|away"nullnullnull-1.0712.64null14.260.0"DFL-OBJ-0000IA|away"nullnullnull
"DFL-MAT-J03WMX"nullnull2023-05-27 13:30:12.76010004"first_half"47.2200010.27null3.730.32"DFL-OBJ-0002HE|home"nullnullnullnullnullnullnullnull"team:home|home""DFL-CLU-000008""#F4F5F0""1. FC Köln"-8.96-1.91null3.440.0"DFL-OBJ-0002F5|away"nullnullnull-0.01-21.85null1.87"FC Bayern München"-20.7800019.22null3.981.92"DFL-OBJ-J017RE|away"nullnullnullnullnullnullnullnullnullnullnullnull-15.80.84null7.1-1.4"DFL-OBJ-0027KL|away"nullnullnull-0.8812.61null15.70.0"DFL-OBJ-0000IA|away"nullnullnull
" + ], + "text/plain": [ + "shape: (5, 348)\n", + "┌───────────┬───────────┬───────────┬───────────┬───┬───────────┬───────────┬───────────┬──────────┐\n", + "│ match/id ┆ competiti ┆ season/id ┆ timestamp ┆ … ┆ teams/awa ┆ teams/awa ┆ teams/awa ┆ teams/aw │\n", + "│ --- ┆ on/id ┆ --- ┆ --- ┆ ┆ y/players ┆ y/players ┆ y/players ┆ ay/playe │\n", + "│ str ┆ --- ┆ str ┆ datetime[ ┆ ┆ /DFL-OBJ- ┆ /DFL-OBJ- ┆ /DFL-OBJ- ┆ rs/DFL-O │\n", + "│ ┆ str ┆ ┆ μs] ┆ ┆ 000… ┆ 000… ┆ 000… ┆ BJ-000… │\n", + "│ ┆ ┆ ┆ ┆ ┆ --- ┆ --- ┆ --- ┆ --- │\n", + "│ ┆ ┆ ┆ ┆ ┆ str ┆ str ┆ str ┆ str │\n", + "╞═══════════╪═══════════╪═══════════╪═══════════╪═══╪═══════════╪═══════════╪═══════════╪══════════╡\n", + "│ DFL-MAT-J ┆ null ┆ null ┆ 2023-05-2 ┆ … ┆ DFL-OBJ-0 ┆ null ┆ null ┆ null │\n", + "│ 03WMX ┆ ┆ ┆ 7 13:30:1 ┆ ┆ 000IA|awa ┆ ┆ ┆ │\n", + "│ ┆ ┆ ┆ 2.600 ┆ ┆ y ┆ ┆ ┆ │\n", + "│ DFL-MAT-J ┆ null ┆ null ┆ 2023-05-2 ┆ … ┆ DFL-OBJ-0 ┆ null ┆ null ┆ null │\n", + "│ 03WMX ┆ ┆ ┆ 7 13:30:1 ┆ ┆ 000IA|awa ┆ ┆ ┆ │\n", + "│ ┆ ┆ ┆ 2.640 ┆ ┆ y ┆ ┆ ┆ │\n", + "│ DFL-MAT-J ┆ null ┆ null ┆ 2023-05-2 ┆ … ┆ DFL-OBJ-0 ┆ null ┆ null ┆ null │\n", + "│ 03WMX ┆ ┆ ┆ 7 13:30:1 ┆ ┆ 000IA|awa ┆ ┆ ┆ │\n", + "│ ┆ ┆ ┆ 2.680 ┆ ┆ y ┆ ┆ ┆ │\n", + "│ DFL-MAT-J ┆ null ┆ null ┆ 2023-05-2 ┆ … ┆ DFL-OBJ-0 ┆ null ┆ null ┆ null │\n", + "│ 03WMX ┆ ┆ ┆ 7 13:30:1 ┆ ┆ 000IA|awa ┆ ┆ ┆ │\n", + "│ ┆ ┆ ┆ 2.720 ┆ ┆ y ┆ ┆ ┆ │\n", + "│ DFL-MAT-J ┆ null ┆ null ┆ 2023-05-2 ┆ … ┆ DFL-OBJ-0 ┆ null ┆ null ┆ null │\n", + "│ 03WMX ┆ ┆ ┆ 7 13:30:1 ┆ ┆ 000IA|awa ┆ ┆ ┆ │\n", + "│ ┆ ┆ ┆ 2.760 ┆ ┆ y ┆ ┆ ┆ │\n", + "└───────────┴───────────┴───────────┴───────────┴───┴───────────┴───────────┴───────────┴──────────┘" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "target_match = pairs[0]['match_id']\n", + "\n", + "print(f\"Loading data for {target_match}...\")\n", + "start = time.perf_counter()\n", + "\n", + "# Load specific match lazily and collect\n", + "container = catalog.load_container(match_id=target_match).collect()\n", + "\n", + "query_time = time.perf_counter() - start\n", + "print(f\" ✅ Loaded and pivoted {container.num_rows()} rows in {query_time:.4f}s\")\n", + "container.to_polars().head()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "ename": "FileNotFoundError", + "evalue": "No such file or directory (os error 2): ...rehouse/tracking/tracking_data/data/00000-0-2ce0e3ca-73c0-404e-9596-10570ff4a509.parquet (set POLARS_VERBOSE=1 to see full path)\n\nThis error occurred with the following context stack:\n\t[1] 'parquet scan'\n\t[2] 'sink'\n", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mFileNotFoundError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[7]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m start = time.perf_counter()\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m df = \u001b[43mpl\u001b[49m\u001b[43m.\u001b[49m\u001b[43mscan_parquet\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43midsse_warehouse/tracking/tracking_data/data/00000-0-2ce0e3ca-73c0-404e-9596-10570ff4a509.parquet\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcollect\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 4\u001b[39m query_time = time.perf_counter() - start\n\u001b[32m 5\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33m ✅ Loaded and pivoted \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mlen\u001b[39m(df)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m rows in \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mquery_time\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m.4f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33ms\u001b[39m\u001b[33m\"\u001b[39m)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/.venv/lib/python3.11/site-packages/polars/_utils/deprecation.py:97\u001b[39m, in \u001b[36mdeprecate_streaming_parameter..decorate..wrapper\u001b[39m\u001b[34m(*args, **kwargs)\u001b[39m\n\u001b[32m 93\u001b[39m kwargs[\u001b[33m\"\u001b[39m\u001b[33mengine\u001b[39m\u001b[33m\"\u001b[39m] = \u001b[33m\"\u001b[39m\u001b[33min-memory\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 95\u001b[39m \u001b[38;5;28;01mdel\u001b[39;00m kwargs[\u001b[33m\"\u001b[39m\u001b[33mstreaming\u001b[39m\u001b[33m\"\u001b[39m]\n\u001b[32m---> \u001b[39m\u001b[32m97\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunction\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/.venv/lib/python3.11/site-packages/polars/lazyframe/opt_flags.py:328\u001b[39m, in \u001b[36mforward_old_opt_flags..decorate..wrapper\u001b[39m\u001b[34m(*args, **kwargs)\u001b[39m\n\u001b[32m 325\u001b[39m optflags = cb(optflags, kwargs.pop(key)) \u001b[38;5;66;03m# type: ignore[no-untyped-call,unused-ignore]\u001b[39;00m\n\u001b[32m 327\u001b[39m kwargs[\u001b[33m\"\u001b[39m\u001b[33moptimizations\u001b[39m\u001b[33m\"\u001b[39m] = optflags\n\u001b[32m--> \u001b[39m\u001b[32m328\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunction\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/.venv/lib/python3.11/site-packages/polars/lazyframe/frame.py:2429\u001b[39m, in \u001b[36mLazyFrame.collect\u001b[39m\u001b[34m(self, type_coercion, predicate_pushdown, projection_pushdown, simplify_expression, slice_pushdown, comm_subplan_elim, comm_subexpr_elim, cluster_with_columns, collapse_joins, no_optimization, engine, background, optimizations, **_kwargs)\u001b[39m\n\u001b[32m 2427\u001b[39m \u001b[38;5;66;03m# Only for testing purposes\u001b[39;00m\n\u001b[32m 2428\u001b[39m callback = _kwargs.get(\u001b[33m\"\u001b[39m\u001b[33mpost_opt_callback\u001b[39m\u001b[33m\"\u001b[39m, callback)\n\u001b[32m-> \u001b[39m\u001b[32m2429\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m wrap_df(\u001b[43mldf\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcollect\u001b[49m\u001b[43m(\u001b[49m\u001b[43mengine\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcallback\u001b[49m\u001b[43m)\u001b[49m)\n", + "\u001b[31mFileNotFoundError\u001b[39m: No such file or directory (os error 2): ...rehouse/tracking/tracking_data/data/00000-0-2ce0e3ca-73c0-404e-9596-10570ff4a509.parquet (set POLARS_VERBOSE=1 to see full path)\n\nThis error occurred with the following context stack:\n\t[1] 'parquet scan'\n\t[2] 'sink'\n" + ] + } + ], + "source": [ + "start = time.perf_counter()\n", + "\n", + "df = pl.scan_parquet(\"idsse_warehouse/tracking/tracking_data/data/00000-0-2ce0e3ca-73c0-404e-9596-10570ff4a509.parquet\").collect()\n", + "query_time = time.perf_counter() - start\n", + "print(f\" ✅ Loaded and pivoted {len(df)} rows in {query_time:.4f}s\")\n", + "container.to_polars().head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Cleanup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if Path(warehouse_path).exists():\n", + " shutil.rmtree(warehouse_path)\n", + "print(\"Warehouse cleaned up.\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "/Users/probberechts/Developer/tracking_data_container", + "language": "python", + "name": "tracking_data_container" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.14" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/idsse_warehouse/catalog.db b/examples/idsse_warehouse/catalog.db new file mode 100644 index 0000000000000000000000000000000000000000..de0809ea32fad02b8cfb3182ee67b1bed7a67ceb GIT binary patch literal 20480 zcmeI&O>Y}T7zglKCuy2Ai8+8GC`F4vYAj{4Z+2|A2Qa}Bh|m|42KBU>oqe`hyz!dd zb(-|(^u$LhAEUP(P%nH3j*M+=#|FDmr4m*8uh#O;?lYdpzsGx+S^xBKKg>uE;^bIl zy2CzTn#R7?b;j5Y^;%M|;g(Yei^GO`*Df5-I=sPle*17e{})?ad&M?7`QO)GZG1W_ z80R4X0SG_<0uX=z1Rwwb2)vy@_p62F&3avXu4TfDD3s(;^2BY4V@lgXlG{$=HYHg| z>AY);dyRVSpspX(zS*zq^CI=F)fFi+5ygX1^utY22>d+|8GEH-G#0QPp#>V>suF-+}20QZ0i>XEvy!P zUc9#1tZOW6`E;888mWn*D>_-+Z@cp-Tx;IdrS6`#yjdt{&ubH<={H8j7_@T#$v?Pu zdGLPxxb!2ae^q82I6LN;GU2N_yHTtp!?-mH?Ys91+)n5uj611&iP$-%8E4b~#cu24 z6RJFSbJsTS7v5B!ewTDykU@Jv-BZ_l_tpBwAeX+YzNk=%*>UPenpUYQ9IK4x@wqSjKQ3XsDUF z>F|;hNUP!zFWOF-7tLzGtA54f#j34N+hkEt+!^-S`6Z28SC_5LWl@^8<5hSd$`&tL z(&NH1tK2bV*$c{2_|w^{+JAZTR-xNuQx#ATndQ-7R(!sYQ)N;y-RA6|cVDLq8L7dh z3uWyF72&Jz+WV%wNOr(0WSP8VtHM>BvK3I}e=XF!bZu^{G-cVhIN6rZi)B$#zpLGV z?|Ray3R#iPt~HtNsw!13|AOUT~*mVs5WY#Z0Vet*cMh@~@fNu|WU=5P$##AOHafKmY;|fB*y_@SX~Mrl}=> zk3X7P0-)0{&C)FCQ1AJ_mj9Ef9UBB7009U<00Izz00bZa0SG_<0`IfHC%rX*>AwLC H)&TwnySQi7 literal 0 HcmV?d00001 diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-9af3a354-c746-4f8f-966b-040290fbf4fa.metadata.json b/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-9af3a354-c746-4f8f-966b-040290fbf4fa.metadata.json new file mode 100644 index 0000000..e7bde2f --- /dev/null +++ b/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-9af3a354-c746-4f8f-966b-040290fbf4fa.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata","table-uuid":"6bda1834-894f-443f-91a5-f9960c731000","last-updated-ms":1775647655857,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":true},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"snapshots":[],"snapshot-log":[],"metadata-log":[],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":0} diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-dc033d34-aa86-4054-844c-b33bbaf9b53b.metadata.json b/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-dc033d34-aa86-4054-844c-b33bbaf9b53b.metadata.json new file mode 100644 index 0000000..591e007 --- /dev/null +++ b/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-dc033d34-aa86-4054-844c-b33bbaf9b53b.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata","table-uuid":"6bda1834-894f-443f-91a5-f9960c731000","last-updated-ms":1775647659837,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":true},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"current-snapshot-id":1456969541151746380,"snapshots":[{"snapshot-id":1456969541151746380,"sequence-number":1,"timestamp-ms":1775647659837,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-1456969541151746380-0-b3defa57-62bd-4a60-87b5-0d6fcafd53ad.avro","summary":{"operation":"append","added-files-size":"1586","added-data-files":"1","added-records":"1","total-data-files":"1","total-delete-files":"0","total-records":"1","total-files-size":"1586","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0}],"snapshot-log":[{"snapshot-id":1456969541151746380,"timestamp-ms":1775647659837}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-9af3a354-c746-4f8f-966b-040290fbf4fa.metadata.json","timestamp-ms":1775647655857}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":1456969541151746380,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":1} diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/00002-2c7bc7d1-e21d-47a5-9d6f-6bc39ac8c361.metadata.json b/examples/idsse_warehouse/tracking/match_metadata/metadata/00002-2c7bc7d1-e21d-47a5-9d6f-6bc39ac8c361.metadata.json new file mode 100644 index 0000000..5e39d11 --- /dev/null +++ b/examples/idsse_warehouse/tracking/match_metadata/metadata/00002-2c7bc7d1-e21d-47a5-9d6f-6bc39ac8c361.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata","table-uuid":"6bda1834-894f-443f-91a5-f9960c731000","last-updated-ms":1775647663690,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":true},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"current-snapshot-id":1114700784421547592,"snapshots":[{"snapshot-id":1456969541151746380,"sequence-number":1,"timestamp-ms":1775647659837,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-1456969541151746380-0-b3defa57-62bd-4a60-87b5-0d6fcafd53ad.avro","summary":{"operation":"append","added-files-size":"1586","added-data-files":"1","added-records":"1","total-data-files":"1","total-delete-files":"0","total-records":"1","total-files-size":"1586","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0},{"snapshot-id":1114700784421547592,"parent-snapshot-id":1456969541151746380,"sequence-number":2,"timestamp-ms":1775647663690,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-1114700784421547592-0-ed38ec79-f365-46e1-aa0a-6ff5e030781a.avro","summary":{"operation":"append","added-files-size":"1586","added-data-files":"1","added-records":"1","total-data-files":"2","total-delete-files":"0","total-records":"2","total-files-size":"3172","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0}],"snapshot-log":[{"snapshot-id":1456969541151746380,"timestamp-ms":1775647659837},{"snapshot-id":1114700784421547592,"timestamp-ms":1775647663690}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-9af3a354-c746-4f8f-966b-040290fbf4fa.metadata.json","timestamp-ms":1775647655857},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-dc033d34-aa86-4054-844c-b33bbaf9b53b.metadata.json","timestamp-ms":1775647659837}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":1114700784421547592,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":2} diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/00003-16f8ad09-c7bf-4e2f-8e10-522378371fe8.metadata.json b/examples/idsse_warehouse/tracking/match_metadata/metadata/00003-16f8ad09-c7bf-4e2f-8e10-522378371fe8.metadata.json new file mode 100644 index 0000000..acd32f2 --- /dev/null +++ b/examples/idsse_warehouse/tracking/match_metadata/metadata/00003-16f8ad09-c7bf-4e2f-8e10-522378371fe8.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata","table-uuid":"6bda1834-894f-443f-91a5-f9960c731000","last-updated-ms":1775647666733,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":true},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"current-snapshot-id":4866667276096503338,"snapshots":[{"snapshot-id":1456969541151746380,"sequence-number":1,"timestamp-ms":1775647659837,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-1456969541151746380-0-b3defa57-62bd-4a60-87b5-0d6fcafd53ad.avro","summary":{"operation":"append","added-files-size":"1586","added-data-files":"1","added-records":"1","total-data-files":"1","total-delete-files":"0","total-records":"1","total-files-size":"1586","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0},{"snapshot-id":1114700784421547592,"parent-snapshot-id":1456969541151746380,"sequence-number":2,"timestamp-ms":1775647663690,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-1114700784421547592-0-ed38ec79-f365-46e1-aa0a-6ff5e030781a.avro","summary":{"operation":"append","added-files-size":"1586","added-data-files":"1","added-records":"1","total-data-files":"2","total-delete-files":"0","total-records":"2","total-files-size":"3172","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0},{"snapshot-id":4866667276096503338,"parent-snapshot-id":1114700784421547592,"sequence-number":3,"timestamp-ms":1775647666733,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-4866667276096503338-0-faae9c91-7f9c-4bec-b401-856370f90b74.avro","summary":{"operation":"append","added-files-size":"1586","added-data-files":"1","added-records":"1","total-data-files":"3","total-delete-files":"0","total-records":"3","total-files-size":"4758","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0}],"snapshot-log":[{"snapshot-id":1456969541151746380,"timestamp-ms":1775647659837},{"snapshot-id":1114700784421547592,"timestamp-ms":1775647663690},{"snapshot-id":4866667276096503338,"timestamp-ms":1775647666733}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-9af3a354-c746-4f8f-966b-040290fbf4fa.metadata.json","timestamp-ms":1775647655857},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-dc033d34-aa86-4054-844c-b33bbaf9b53b.metadata.json","timestamp-ms":1775647659837},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00002-2c7bc7d1-e21d-47a5-9d6f-6bc39ac8c361.metadata.json","timestamp-ms":1775647663690}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":4866667276096503338,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":3} diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/b3defa57-62bd-4a60-87b5-0d6fcafd53ad-m0.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/b3defa57-62bd-4a60-87b5-0d6fcafd53ad-m0.avro new file mode 100644 index 0000000000000000000000000000000000000000..08a656d5df778b83db290b85bc6e4d91c068526f GIT binary patch literal 4490 zcmb`KYitx%6vv^(Xp|H+#*hf*GGElVEju%n?k+(Hp&%-)w3P_WW^#AtZaZ{#W;%1H zWyL}oKzSJbgf9Y#(fA4kgAt*@v`QpujPb!YAR&<$NQ_ZG7-CdB_fBVaX16SD*`#UO zJ9Gc%%Y^d7T)VD`a}M3A!d^#Drkt z0(+x?soJ*$))J9Dy4ef=J7mDq9Rja|3K>rLTcvQTY87lkb*fwD%>?q;a6<(-RB#D) zr@6T$+*}|OYnWnX3uxTj{cJ+;@iZ|2$pmm9Ce;Cq)5Ph(Is=p)USMO#|Ewn0ChDA? ztPo#;H|vrM@|FYeng)sE0)aZ|K0N4H&8nplwLv3!15?tVS|$Z6cQ8Ni=g)l~US1Z2 z2mxl*a)3VygM=VTes~Z>5L3?$pK|9>5?=Nrj0D}J!f*mbc9__9`z)%kpiYST+r-kg z9>MesgFQDa2zD0!0H3jEP$7fZqqrnw0x?yhm|h`AoHAK?rm}h6Ac|X6yJe=@JTsVq zGH0cd;8a5}5yUbfQI7b=qreziD)1B5fL-M02V#NDAQt-svK*EDLXb`Xv@C9%hK*@o zxuIk<+)&ZHIzwAeSt@2Fh@RLabyt6mD7vZSijB&}12GN6G?*D*BUYZOWq1WsVQ*Bbr$B9@SA<1h%6#lNxX3lT9wH zA3$FERKu?AcZfJ%!x|!vk^}BA6#~gIOw}pcd~5`H6v(5TVddf>bZtYYij~j1gjN)S9GT@p^Q8$Q94?4K3UCby zoMw|%FmL#yRn>F6i@88?4u+t>k66OP1p>nxMY!-(dN{>WE%R{c^gLX{Zr3njxX5FZ zC(Icdp~NZZCP9uhgaQxQ{1*PLflUh61Q_nz<}&D(1I!%_il_1t*D~2D$uA()BED`l z%&N9STz2V&W7-81a1{X)?4kmbvhH$M{E;e{_#7Czd2E7(0ygzLaj60_PEi=1RX;MK z{nB90JNVzjuMOn7#_BHAd;6ESpGnSt;bO;z1AE^XssCtx&#rg&9PPv_yX(%~dE&~) z|L$IWdEWWmkM9{7Ido6M_`+1%ru3}a7hfGeyX^FiAM1br>yJ;iZ!ccIKwdh!_>m_v z_pVAT-1Ok)^AEMXN`E`_`VY$wtbcOj*8P8mQu?u<7f)U!>Mv zt>663!2@rvSlNBWJG}Aoz{)SrUPjHQ_RS^pJGLG_Hh0DN>Oary-FK>G-MgYZuq%J8 zZQG~9nD^Lcr=M+Ha;f)$Ro5DCyLj?0W$lqgcaNWZ<*noE$&p2~fBm-oyKB#nUV2X& YeN%by^Phj((YkXaH}=){8_EIv4;-VV#Q*>R literal 0 HcmV?d00001 diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/ed38ec79-f365-46e1-aa0a-6ff5e030781a-m0.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/ed38ec79-f365-46e1-aa0a-6ff5e030781a-m0.avro new file mode 100644 index 0000000000000000000000000000000000000000..36a0256094c75cc95b9a0dcebc682297bc08c5a6 GIT binary patch literal 4489 zcmb`KZEO@p7{{UVfe@%K5c%MV<5nf=wcPGidUpha0*$B?q@a@a+|2FHwM+MQ*V);U z6Pkh{C6TC6g9(NY4JPsd1K1cf3Nea?_)XG85?>n(8sdA55(!41neFcF-j&jpOPZ$L z-T6Py{GOM2WECYQm$)ZE95MjAESVqj%1_w#1)-uK~!6+ws) zVAgC3{81Pr1X1$CgCK&Kd2aYtIFC~Bu^(Y185WZUk|?sm!j9W(GnEH*T-0ACSC(~2 zmS>v$y=g^Txu}>nKYWx?1bONANaT7Eg%zCR0 zrJ~`6iWb!w+IrO1FfT#0Wz#ye-C(__y%UKnBk{Zt`o2>&79yF!dq&*CjCK=NOpUE6 zM}Q|~PzF>^DSSHt;f+CwA|7bOJjWzSf7=)puuF6VNLVl7to>p}yqM!i{L)i3+k+wk z@t_Yt4CPcLt^FdYSnm*Jfb1gj6nbXz^MHJt&N>Fms5rZX|`r&RdqY&~J}$%Xd= z$V;DU*tPu*5yxxV+X+<*z#XPSAQ^2)%6;`jt1Rbv+7=p_b|X-hPmXrNQ%7m5A6vl+ z30854p5t5@VqFg=g?H2RHFQ(uD&2HKi0L}EQ^9H?h;XQ?!CfazlJS8)9U0uhjw$)R z<{?S1bTqEDG+K7V*a-3{kVgg6F2qCVI;O!?yI6Dyt0{zZWR?rfS0;#XxF9Ae!8Isw znom~2yy1^llb++9+y#aUFa!mD#1bAZ5E#BF!-c2T!zq?(m50lx=V3d)UBiUoB9Bj= zFlXq55n3`Vf~dV61s?MGE&N*tn+&cAFxPl7m&#!zHSZ7 zstzSCzx2W}?Scuoihv1zQGrR>aD^-WNEJ+c6ozgQo8UnSTSk$%Oa&QQmIh|s!Co|q z&wcRjzkix*ZoBYHw&Ba`ynQP=POZ9OXlc{EbnW`x?|n4VcObL7Wz+DgU9;{y^!8mp z$?yEW@0$frKKS+RD-WGp)cx#hLoIuUKk0bjw0e2jh1K1IJMC}3dus6S*<%YwPmF1I zpZ|E|tr7Vz`q_~8!#Txy3#AoH#ju W=WqX<|L~UMhsPEtwmnw8|NjC0#;17z literal 0 HcmV?d00001 diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/faae9c91-7f9c-4bec-b401-856370f90b74-m0.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/faae9c91-7f9c-4bec-b401-856370f90b74-m0.avro new file mode 100644 index 0000000000000000000000000000000000000000..3148d2b3e9ae266a43f7f8c48dc63bf9234b279f GIT binary patch literal 4492 zcmb`KZEO@p7{{Tl#3(5rcrh4a#?{2c-r?RO=%pJk_vUVuKEmauQ{(r!grZ6%AtQRLb@4mJD6W$qC8A zCH_VUGp+9-SW86q7}hrU-z7Ue!zJ)KsF0zAzf}phYIeyX%wUFX-Atf}O&ThsLj_7O zo#*D}aC3<;tYe1N?VxdU_wxzK$CKm$QVHNdEM@>0H!0}AItP>=TH|2X|Ewf+h}O`X zm*Ok%W?gDc(RKk|vY)sV2-GPnaKCG>)oh(;jXEisn2|=!wisBso%^{;JokOr_ec;T z1ei731^y@u5`rlC;Xx2V%sd)C70#m+yzEC9NruIwp#+NTu&_h>Y^L&{PK)~6Z_JoX7>yBhz6Ae{hcMcf1p2eZCP zL#b%Ep`t}~hPFCw4f7I2&vbX70fY6S_RUCS8HwkG(D$9HvJlA(-ZSDB7W6e?#Z=jv za|C!&24&#NDTRMdK=|pPL=g`(VxD7?g#XzX6)+__0wk=DaMpe?BVNpLB!20sn(aXm zfq2jdAck@(lGc8al&yD(GC+0_`3e*nzA?@?ZbDF}#*0KXEVVGmNM$X{sc0UFYAhS- zu^BcI*Fao@oAEVb<*C`GSF%+8Mzwkd)TVnySoG!0$F7S9`KKFCR8qA@DQYJHiK&^M4t+x$9tc}H>cU-dw)|L8X9Fxp8r`h}R%8xX zSvX)tUI1XFc7QoABzQp%^9~hr$HLaEFw?EI%+_=UV5YMRhnddAyq{Cyr?a($$tM@y z4X%7%rEdY0z3V~#_At?{n60Oq0^RzWIGVey9w9Jln##2XWtsPsz z3JF$mh@Ruz7-CHiCWUv?^euE#r7=#N?wTE*o)d~+==I3EMzg@$GL6OHN zPna`w!iZZkEP`Bn00kcM`7Qig2b&D82{7EzwH4597nr*`6i@Xfu6d?YQd~f0i+IOI zm{lE@P=4u!W14~qxQc)Yeo=u**`UG|f20Z~J{N{=5u0G4ge{{;C{sbk9h8O^;YEud z-2dUoPrr6=_-Ewc=z+TP_1^0zpFH*Gis1)FJ~;f*Cx5J*7$wIizDtfvhj!t+&N#&@ zry3|NK3iXyEL@~pp4)PD(T11bd135}RsH{-erfNr^S@1Aynf;GU&aM_V(YzM-ueCV z$Im8LH{HFtsco43JbvKo}_RQXExnu9|zb)6v^4YWR{eE=SnIov_&sRQv>xZ$i z@vYBatKaxq*S3z1#@#*Ro38x0|LChLCU<@QcJsIEdJdns+h-lua=yKfPzM(bipw;D2R@tV#d? literal 0 HcmV?d00001 diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-1114700784421547592-0-ed38ec79-f365-46e1-aa0a-6ff5e030781a.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-1114700784421547592-0-ed38ec79-f365-46e1-aa0a-6ff5e030781a.avro new file mode 100644 index 0000000000000000000000000000000000000000..a83c360826eebcefcb86866855515ba1a21b7558 GIT binary patch literal 1900 zcmb7ETSyd97`FR`2#v6UifydSYHenl*>w%dvYTQiMGqA+o}D@CPR*^Evu-A9A=N`f zQ4}N?DA+z|L@0*ULy;-wLod=xMp*h_7b>Yhy3v_+XPb3&wbyh0@B9A$`!D-_$EGbv z*JYsU0R=e)el0~&w1*@;IW+B}7}~>lU8_}~K^Zv{!f3|rb$c0_q8Q3UyEo^MD|L7x z2xSgB<)Gw)+H}`)zoJP1IV+&13yM75wHQ=rN;aqPkXy(@zX%Xak3Q*K07~G_k}3jK zs*uQa5E|qZjVCrb2)_VDUdL}os|XoL(O(J3f*RS;cOg-4iq>-0tz^GA9O_ZArao_7)L{da``walto2UutowJdxiuQJ!QVvjOaA18iW%0=8!?2{_kS3nyZ5CuIOU4|&!c zlksZaJO#X85TVX;O3+?DNn7wX&)`WxN4VK+G6t&$HU=Jw5aAM%5m+s-5sW&ZDWM4p zpsY=|w;*0Y5{Bd!niV7=uzF}uAwIYI*zqAPvv-t`gQAEd{w*xfG*D?^u!oXyE@I1= zgP#NhC1ZpCMJHcxWBP$0>v#;KDx){RWb|5wz^5o81hRd3M&bAe2S+bR3{4sN#EwI} z3c{o*#h>^vfuM+Rx`ukfQj1CmYOD{Brfr=m%h9p$RwAeqlduL=bwXClk(mOkA!MFN zjUjW0aCj2!HUt-)Cc!h;YiI($*h0QimJ1KP)e+ojIp)=93wn z&#B@La_Ox8b??154p)DC`?8=m%~N)%y6;iJ&P99fW!+x!AmeFDQ|_uyDNBatW|j@4 zUUcploPYiGmv4=k;l5Xl^Cs1mopG-Fe1BUcFH(HvW^c`rZ6#${_4nl;%{{&+JtJE| z@8{5i4mpt3($%&1g>!b(?|qHKk8_J6cXA6ysvx&&L!qm4d0ojO|Aokz?zC(A)%N~f z-d#f@^RjDBw|BQZ8~pS8!%x`y$J^8TaZ_LZo3^`yCF%9WZReJ)UkDr4sLgfT%WviV dynOoAz}H{zzJKNJpQ_%na(GMrw6kMt{tHcyf)M}! literal 0 HcmV?d00001 diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-1456969541151746380-0-b3defa57-62bd-4a60-87b5-0d6fcafd53ad.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-1456969541151746380-0-b3defa57-62bd-4a60-87b5-0d6fcafd53ad.avro new file mode 100644 index 0000000000000000000000000000000000000000..b1ed9f2bad58ae0a490c1dd4f9706287219ff4be GIT binary patch literal 1824 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 vUe^>e|M8k0*b;=MN!b{cl_V`-uK?0 zcvXs>V_#aW12}qBlqF_N1|SX*(+p))Gh&HYs|T2d7KbEfiS-(Bh*TmbP@H`OM;Wzl z416ZBve6Us;Qti(vr$h0__`LwXpk-8ZDoS3n#2u&(KC9I=#U4w;DH7x-9ckN!I;tG zfM!tKV9DM3(Rg+HiT-2pobbpQcr zYf`2Ht0Tf*X_xm>3v;YF^FcEUZBhV51CvrsVx*C>e4+Bc^9XyIQ0QGu1C=fKvP7Tba=J%m!`1(R_FceDAT9U${ zc<^o+7R1(alC2Qix`zI``+Bg3C^ISzA^}ySyUYbmqsY-}QUlbE8lc5c1~{rof`REe zXix@qByWHDjHAW;!9Clm_!2h15YVvINhVL!5U3FQ+IYM>;HxC+2} z+qji?)~!~L0VUn8Z@%FfPLL z17Qmpg0QuhAS9(`e<6@g5YSe2#l6|AC_aK;di$)1$v2+|t zQ7F_}SRDr7P`{ymnFY2l75=n0O3E|AmXpwsaIej@vg1tI9>8!<>_UKLQWjuY=K+>UI{~n)>i}l)LO8r2hgpTPNrym*cEOYf zb|skdAPA;BxbtAjgKV<@sK%EEcO{tY#>DCY3~wIAntIjg-BWp zlFYv=p_K)&JPj0rbXZem0qsNk&Z#2{>^f{YtPl<>TM&%~c&&XOh+R6EWUQLX|A3mx zKA@)W1X13Fa>`-#E)d~RRpNj_*+oYnNy|k-ZnZE9(_PHxs(>VY-)M{=)Rf7Z2IiAR- zz@YE~58Ic|!-nwi;)23AOz@)BHp7o>)m9k>bS%S2_Ny?sVbF+?-8|u*p#cn_a6JJK zicCk$8M6DE@V5rWWZ?A#E_d|kR-ls<#GKN==4rcW`@L&Qa=TW06^{;wyQ+Z#G<$1@ zW15B`;1vjlU@u5;Q`Xa5D*Q@i-uNiEbaS;aKfqB!uZ3TcDWMz6B+l-3+`+W6Mz;28 zZ0OgId@>Rbb-&&VzY?#jO7Q5}_(h>H>3~;FLd*(jc=-79-Z*}8@!>T3?4GwX?A;5m zJ-cxAXl(41dd>2>p^cx$iTZ=$d$ARxm-H++zO;8`Sl$Dq@31(h%!0j%B|V?W+|pj0 zZ4!CD%zpW44pO=B_^W30b>*$6-4e0PdSrkp~S#zYP* zI)a~Vo?A-Ek{1R1{Ga@l{Z)R*OXpKRO#HL;bVKmmJIgqbHLFr{-@DESO4@t z-E9?`9#77?vGKviSL-j8zW#pyU7rzCJY1)`R9^JZCtqYc{W4DS{1mc=4zh8|`X?RU-eqR3K)WzqS zLry0(IuDUn7zR0-pS?J?#!@BP-aC6E% zn;{MDn~(Y~=)0=d+$Ebtx#t?)>^@N$M!y*5lCjw7UyF*G20a=RIxuNoPuHPuktLO} zmufxkZX_0Z_gI~0S9#%3Ho4NSXvNE{{ONbWCj5*dbo$6|3%69G(Z#pwol0JLH3lwBM#YtW=K@j!9v>4gS$k(hXy4h6cF?f+;pU=r4xbO7%Vb{I>yH)9a-$f2A2>X8L zvh@YmQo+R7aOZ9!m!~hA4m}Gi`yV;`FE~auG~2K8>)nQaxeoZ+Se^4kx7@PYtA}%# zqFkSx!{u3Lx6*IAM|CE(`c6&Cjjqz&e`A?5r)9sD!k)oQvjjO$paXH)HvD?1TTl=5r zW2<5uj}18hk~j1|W~w#}!N0y!XWVxI|Hsj!r-ikrzB(w~xMAP)Dxh_K{!&q+t3F;_ z_B3!(4&lmKNBDQ`I8H*R5R!qlElrk&vte?9x#vhxd` zr^LI;iyqC%@tk#d(9W_rMb^zG)BMC0-^_ZR|8@o$TK00m%+q)JJMP=7o>`cBR55~} zhm`f(_Qu&iI^tYPqo~le&yhX%otHU0Oiz|CUJyET=N;Fu3eWk|9{&{Pxd_SI^m=gq J=E~Nr_&=$bj~oC1 literal 0 HcmV?d00001 diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/d962db46-093c-45e8-be4a-0c6d572dad92-m0.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/d962db46-093c-45e8-be4a-0c6d572dad92-m0.avro new file mode 100644 index 0000000000000000000000000000000000000000..31c7dc227f208ba25227159d2e644dd798424fd0 GIT binary patch literal 6365 zcmb_g3se(V8s@3-9T0(9w~i4lYQRh$CPWrRQ9%(Aic)BdlVk#sBok*QC?MhkDvIt> zQQXB^-%EXC(fUN8g7{utAFEbHe1Hl)q6f9-6y3cuLna9*3gsLQCz-kb<9`45fA_zc zrkdl}yCD)rNitV~Kq^;R)FZHZrvG2T`lrVP~Zd zY6P@RQ6`}_TLit*ChuV@b8I>DQ8O!T=Abx%rYO~z9y8>MUF;JurFd;b8!G44ZRj+Xb0zrW6)TqVOvL1+rKmfQw;)s#Z zVz>c_2jiBcKx`=|=?b2`Yv9jGFn}^dNWWx&1W*kpp$C$TWBzJPgQ|OLP^}&z(cWsz zNP=`Z8iYhV>G#ZL-UV884FcpswQh*dTSWX>c30og-HGr_*4w*m>kVn!Arp3X?} z()o~$g%L3ki5N-CL9Xp4qAfKpFjVg_1 zg9-)pwsR}$s9UX0k1B~aeTzHln?hVk1!$41(CYA0OVC`z7LH(%O>T4*fM*S+2KezN zg18XF4~QzH9HMH^hcH}C{eu9V0H7`EMx$Xu$i$X`09CE!wsaU9D;!fJ)UgG{M1?`~ zbYvnlF&d&-hT8Lj_Rl2MDuN9&sDqXk4>m%FcEff|t5_YV0DZD6=L;uNYGubgx{+CQ ztgLR{1jTEe8U>t9XjThLUs;ERR?Mf2EX#0MAf#ZlR5sG0F%U&Ch%}q@D4&5Ffm8^A zY9LSwG7+U}%%~AWsgZptn3f0XX$H;Pazi%l#5AWhdIa5m5T=C~4+NxRf>E`#4?$O( z(4~T@z^GKEkZ4N{fd#hwWyS?|n_pohqK7CS5aojk!}ze11)%hC+ z^JGMCrj@}G)Utm&8(F%7D!m>{(5VsqSM(eTfics>OIJ`uu@g+{0n`0c5ffD+1|3MJ zfk+JeQGn7|0)A+8M3Q~E=!6(Tw1zSO`0hL?0|1ml!JmLKp#4w^1)U8g-8+0hOts#u z0eVFrS`cE?KoLlUJ+{VM|7Iq%M6kZUG7vM0ZPu!W)ry9eh9=)J(z@) znu`8}nhHOnrtbw&)QNIRWA#1|!J(=&pd>;KIxI<=FA^e~fssAkc}%VfK++E#jT4M& zJjFzhtH3>EaSy5Vn99C{gh{U>m6%pbpk%8?5ZTO-Y!;bLa;(~w`Dd3{jI>S$F)fO3!YF~yEsh`5Ygh4=NYVrhg zh6W{3+@Ld}5RT1*EE!Vs8~a%UVv^u`0){)nzXj+R4lu_xpm|zP+T!<}lJui|Y zz^rP*QG&X)gJYTiA;1*~grF`+U{cl*bSlh9WtsSJFm%(ku{SQmvo>)S;Mb4U6ghv4&f?&qzq+}V&dVaKwyj)^X3#Gc%; zE z4~^4KpBuJdqAH^Qz}S+LdF%XriQt#sOXuOK9#NOvh{^K3Wg9O2G<|5sOOg6s2Bzv3^8 zHC~FMF{!V?=GSc~a&`GCbc|?&@H8 z+`SdY?sL{&4oD{$q9r?G&$=QQ0MUC^4hm3?JNp*7WZDX=*2Km#L?5)R#?i;$> zaMZ{Ad41%8wGS$mN~a%hgoLU0kY!g=ZUqO<|2)c6xY%ptIc}eY7dJ(|c(AmpN8ro1 zRbw_kt;K?lj}B})T04m&SumtHT-$HL+0o_6YYGhga>|DmRLjo=ri7oHvFhfEB(>q< z?x5jMZdSNn+V%P3nYMDN96PZwO9pQn;{HQcTWdKd0`H0bBrhbJPN zit`SwY1s9@)dh)BWp4&xQb@ z+>Hq;_47yI^E_+*c`@_w)56{5U7*MBG_z5E-O+_~j3C-d;e znO~fE^7bEhytZH2VHoflugQOTFN_RU6<#|m-}uW8AC@E=O)0dVJS3Vr~D| eQ)HTj-EP zha%R+cGrz1aD_q>k4Z=bi9zA1?m>mxLFiJ)zS-SbM@L7ye&72%&-*^_k3ZDEXMeRO z0YwYT$j!4ZkJsyIp-7L9qIBt{uL8d7bMknr+8Gvv$&KuqB z&LJL!oo&~gd3{L4>YQ6>2vF+VLWMG^2r|~#fW{^vUe}>D1u`_w%qYdkveAU+CA1OF z$P&UPZw66bJCM-;y3ha%xY-1@W-RhJ*J2B|V>oZi09Y2Xv@sW3)$G{?p5q0m(Tp6i zmQOaB@D|VDU0y@D*|Z&l*#ip$3k8U933dc#3oHZ$2UK}zn*u0v)2%HiRbazlZ=q3v z4T0H1YYL^g)i#e0X_~zrLf?2CvA(cCRY6q8U=Kz8J`|VHho5)^MSX+UqwUvQpMJng z8Xm*EO5p|=j4h@i2+6Vlfn;5t0v!L~;QR$CqA5L};&CWdK^Qc*@FzY@AR-`~uCBge zsRelms&ojCre&Qe)6p^UMiJDA9atlZvMsCW$n1hu7cx$y+D!V0SZoLF0T`u4hyV)^ zuvld>`ai`>_gUX0B3Aa|;ryWyKR5lZ_022a(%ZiB`+G0EPmHBT`*SlhCxe;b^(p#F zBAZAA6Y0#=?y0qSa4bDaKMhXM-HBx8dFokhHXeu{%Vx4S+KD#ria-NA4d=VQD#Xp^Y{0o8rMSEgT{L4;@td`Q**Z~ U7QW3NUcOgRQTDy9b8|`m01RAb1poj5 literal 0 HcmV?d00001 diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4761525111313210917-0-4665052f-9f33-4c4a-b55d-e1fd2bcb223d.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4761525111313210917-0-4665052f-9f33-4c4a-b55d-e1fd2bcb223d.avro new file mode 100644 index 0000000000000000000000000000000000000000..e0260358df2d37308f6c03350f9f460b985304cb GIT binary patch literal 2000 zcmeZI%3|aZE6z(SD9*?)(alUzGch+aG&M3cG&D3eG&V9curxGRDM&0z%`4GGQl?^J zZed|&W@2ecSfX2&T2!2wpT}e* zl2}%hua}&klA6qylA4y2SduCR7Aj88NX<=LCtj^oQdy9yWTl`~l$xAhl%k}gpp=%G znv+rt6pOA_%1g`z%jPELWu~PTm&6w&mSn&c0i6$2W@=yn)CDuAxTGjEFCC;LB|jNt zuupz+VhPYS3ZX%s3gww4847N}3Sj$Fm1=bqu-K84nwO5n7DFUka`J(8py&n$SxF|y zp7`Q|)Z}=e%V1uCxzY$pe`a0@s`ljkyb@rLqiZrW$EOJv0r8Lsz|d@pq#4u2xtTz> zLZc9u31)arNK8pdO^Ju45^R1pCPIH&W=?8xd~$v%ruPj@ko04OS!zXQaS1T8w-PEx96;0c@aebTj5cdzcAOf~_n z47MKDqZjvkF*^#deK2hkY07_~y{3A5`R?hPg_}Agaz)-A`Zv?K-Ba()yo*t5beNhV zGKB??xKEg(*)z?q;YMextH9PTB1#gM+B2U?|LBsxeA=~;SByh(wpfp{7wh+^; zOIUPQsQMbHm^!^Z);qULY~_j-OD1;hn0WW0!?M3F zf5g_Dn|U~+6vzW@LJ%O?NMxM?$y z0)nVm#kry|pY?dXZa3>=Sk_H@T#TEZ9|RgyaIQIu@pu>??O?nP+UuiPn)ak4SQ>%~ z4|A1}?1x&sb520jWPo$apoTZa1fNigRmwlMS6&! z@;dPbl?Rw)W8boQoxNPd#rC!N_J}^DDst@OXh={g8b^gTsYoi(7=Fl$kJ)TiwYhM=T!w0%g2IfPb`qp0B)!O7O=|%wq`7gB-h~7j$p8ca3Y;+| zWMX*L&&!R&#BL4Xp(q=Z-mW(zC?Lk?(a`2+>f z=B8U)5Un5vLu?C;3StnLJ+!6}om(_^d>p3PE1;B+B$0@bg$0@h!a9a{DC^f^cp1Iq zNyJdrH+UpE_Im5n4@3o#F^s4T-2j8pXBq;(s!9+j*5w(3lOGZsxgf)6TF+ZyjpHU>RQ`cPThwyoer-}An#?iB~6+U_3h zd{^1jsVOCmb#MGjTW@{rT70K>cEG1N?^b()t8rV zbg@;JE=l(6_VmZ~P3UIMlV1OBn=S8YbI 0: + print("First 5 rows:") + print(df.head()) + print("\nUnique match/id:", df["match/id"].unique().to_list()) + print("Unique player/id:", df["player/id"].unique().to_list()[:10]) + else: + print("Table is EMPTY!") + +if __name__ == "__main__": + main() diff --git a/profile_load.py b/profile_load.py new file mode 100644 index 0000000..77abbba --- /dev/null +++ b/profile_load.py @@ -0,0 +1,146 @@ +import os +import time +import json +import shutil +from pathlib import Path +import polars as pl +import numpy as np +import pyarrow as pa +from tracking_data_container import TrackingDataContainer, TrackingDataCatalog +from tracking_data_container.schema import TrackedEntity, DataField + +def generate_synthetic_data(n_frames=146000, n_entities=25): + print(f"Generating synthetic data: {n_frames} frames, {n_entities} entities...") + + # Core columns + frames = np.arange(n_frames) + timestamps = frames * 40000 # 25 fps + + data = { + "match/id": ["m1"] * n_frames, + "frame_id": frames, + "timestamp": timestamps, + "period": ["1"] * n_frames, + } + + # Entities + # 1 ball, 11 home, 11 away, 2 referees = 25 + entity_keys = [] + # Ball + entity_keys.append("ball|none") + # Home team + for i in range(1, 12): + entity_keys.append(f"h{i}|home") + # Away team + for i in range(1, 12): + entity_keys.append(f"a{i}|away") + # Referees + for i in range(1, 3): + entity_keys.append(f"r{i}|referee") + + entity_keys = entity_keys[:n_entities] + + # To make it easier and faster to generate, we'll create the long format directly + print("Creating long-format dataframe...") + + # Repeat core columns for each entity + long_data = { + "match/id": np.tile(["m1"], n_frames * n_entities), + "frame_id": np.tile(frames, n_entities), + "timestamp": np.tile(timestamps, n_entities), + "period": np.tile(["1"], n_frames * n_entities), + } + + # Entity columns + p_ids = [] + t_ids = [] + e_keys = [] + for ek in entity_keys: + p_id, t_id = ek.split("|") + p_ids.extend([p_id] * n_frames) + t_ids.extend([t_id] * n_frames) + e_keys.extend([ek] * n_frames) + + long_data["player/id"] = p_ids + long_data["team/id"] = t_ids + long_data["entity_key"] = e_keys + + # Attributes: x, y, z, name + long_data["x"] = np.random.randn(n_frames * n_entities).astype(np.float32) + long_data["y"] = np.random.randn(n_frames * n_entities).astype(np.float32) + long_data["z"] = np.random.randn(n_frames * n_entities).astype(np.float32) + long_data["name"] = np.tile(["player_name"], n_frames * n_entities) + + df_long = pl.DataFrame(long_data) + + # We need to return a TrackingDataContainer. + # TDC is usually wide, so we pivot it first. + print("Pivoting to wide format for TrackingDataContainer...") + t0 = time.perf_counter() + container = TrackingDataContainer.pivot(df_long) + t1 = time.perf_counter() + print(f"Initial pivot took {t1 - t0:.2f}s") + + return container + +def profile_load(): + warehouse_path = Path("./profile_warehouse") + if warehouse_path.exists(): + shutil.rmtree(warehouse_path) + + catalog = TrackingDataCatalog(str(warehouse_path)) + catalog.initialize() + + container = generate_synthetic_data(n_frames=146000, n_entities=25) + + print("Saving to catalog...") + t0 = time.perf_counter() + catalog.save_container(container, match_id="m1") + t1 = time.perf_counter() + print(f"Save took {t1 - t0:.2f}s") + + print("\n--- Profiling catalog.load_container(match_id='m1').collect() ---") + + # Internal breakdown of collect() + # We'll do it manually to get the breakdown + + # 1. Iceberg scan + to_arrow + from pyiceberg.expressions import EqualTo, Reference + t0 = time.perf_counter() + table = catalog._iceberg_catalog.load_table("tracking.tracking_data") + scan = table.scan(row_filter=EqualTo(term=Reference("match/id"), value="m1")) + arrow_table = scan.to_arrow() + t_scan = time.perf_counter() - t0 + + # 2. to_polars conversion + t0 = time.perf_counter() + # This is what happens in TrackingDataContainer.from_arrow(...).to_polars() + # Actually CatalogLazyTrackingDataContainer.collect() does: + # container = TrackingDataContainer.from_arrow(arrow_table, catalog=self._catalog) + # wide_container = TrackingDataContainer.pivot(container.to_polars()) + + # from_arrow is very fast (just wraps), the work is in to_polars() + df_long = pl.from_arrow(arrow_table) + t_to_polars = time.perf_counter() - t0 + + # 3. pivot() call total + t0 = time.perf_counter() + _ = TrackingDataContainer.pivot(arrow_table) + t_pivot_total = time.perf_counter() - t0 + + # Reconstruction of schema (Lazy.schema()) + t0 = time.perf_counter() + lazy_container = catalog.load_container(match_id="m1") + _ = lazy_container.schema() + t_schema = time.perf_counter() - t0 + + print(f"Detailed Breakdown:") + print(f" - Iceberg scan + to_arrow: {t_scan:.4f}s") + print(f" - to_polars conversion: {t_to_polars:.4f}s") + print(f" - Lazy.schema() reconstruction: {t_schema:.4f}s") + print(f" - pivot() call total: {t_pivot_total:.4f}s") + + print(f"\nTotal collect() estimated: {t_scan + t_pivot_total:.4f}s") + +if __name__ == "__main__": + profile_load() diff --git a/profile_warehouse/catalog.db b/profile_warehouse/catalog.db new file mode 100644 index 0000000000000000000000000000000000000000..e860b6af3fc5938907b200651b7537c28407c693 GIT binary patch literal 20480 zcmeI&&rj1}7zgln{3ft@&=5sTjV7>!vX-@5rw5(5L?ePu&=5kJx7|~yj1}7s;ks}z z{z>?sa4=pxbM@%E&24PDNk~+P-=<6Zw$GdQ$>(Vg@7uk4x#)%@IiA16LMbU+5kygV zC`p1K4DsIp|24JYZ?4!k;)GJb&86B}%$ckQl6zO#~zwjiRpPAS7xU{sqA}y^hF3w7$Bh4JThx|zH zz}%*TGeqsNVwsw8s-h_EBfiK#eV)y)NzZa?(#+{3XQgH`v(o87C6{eu(RIZ<#+ z1=rrPW7KxIK3nuy26!)#k=YK>+w>aEBx`Rn)24ql6ImA(n} zQ)OC#-D7qrWCdPlv*>Y2*DEzcYwpzo@A|aodgZ`8Lu`-Iw6du^v9nU^0x!?~$l%0s z{CDN4bqQJpski(1J2hN=ug0SVAu@I%jGaV3obNqDF9<*Y0uX=z1Rwwb2tWV=7gC_& zi^G#s)0OqgdVw5PE{37c>}|KS**Kcjsh#5%>0Tm{SPjSz5`0>0@wEf8x56OtnD(ex zT{uXz^)PL(6f(EOuk-L~fAc-_X^T$+sTtWksST27Ob!!#x~`~l%2BKgIgU(LG9_yT zGGs&5(z4AgR?rN~W+`K?(P!>$;FbPCnu=^CS&A`>6NDMb@2Jfv zFHR(OB*czlpD*Mai=-d`0SG_<0uX=z1Rwwb2tWV=|FyuK0lr=$_FOE9+~zUUBONhq z>sI63P>g*M_>K(%5P$##AOHafKmY;|fB*y_0D(&?a7*M%0N1Z|ECJBI8-zjVm#O;w rUyOYf_>K(%5P$##AOHafKmY;|fB*y_0D;RaaHF~g(D^rj`WnD@(OO;~ literal 0 HcmV?d00001 diff --git a/profile_warehouse/tracking/match_metadata/metadata/00000-4a6aa9a1-65e0-450f-9be7-7684faeeac52.metadata.json b/profile_warehouse/tracking/match_metadata/metadata/00000-4a6aa9a1-65e0-450f-9be7-7684faeeac52.metadata.json new file mode 100644 index 0000000..94ab754 --- /dev/null +++ b/profile_warehouse/tracking/match_metadata/metadata/00000-4a6aa9a1-65e0-450f-9be7-7684faeeac52.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/profile_warehouse/tracking/match_metadata","table-uuid":"86f5fe78-2067-4062-8790-9b34dfb2a728","last-updated-ms":1775647690681,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":true},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"snapshots":[],"snapshot-log":[],"metadata-log":[],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":0} diff --git a/profile_warehouse/tracking/match_metadata/metadata/00001-9b21cef5-89c7-436e-83d6-51ccb7009828.metadata.json b/profile_warehouse/tracking/match_metadata/metadata/00001-9b21cef5-89c7-436e-83d6-51ccb7009828.metadata.json new file mode 100644 index 0000000..7a28cf5 --- /dev/null +++ b/profile_warehouse/tracking/match_metadata/metadata/00001-9b21cef5-89c7-436e-83d6-51ccb7009828.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/profile_warehouse/tracking/match_metadata","table-uuid":"86f5fe78-2067-4062-8790-9b34dfb2a728","last-updated-ms":1775647694517,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":true},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"current-snapshot-id":2219873109471251602,"snapshots":[{"snapshot-id":2219873109471251602,"sequence-number":1,"timestamp-ms":1775647694517,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/profile_warehouse/tracking/match_metadata/metadata/snap-2219873109471251602-0-e27f5ba5-48f7-4cb2-ae56-16a5fe7fd851.avro","summary":{"operation":"append","added-files-size":"1525","added-data-files":"1","added-records":"1","total-data-files":"1","total-delete-files":"0","total-records":"1","total-files-size":"1525","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0}],"snapshot-log":[{"snapshot-id":2219873109471251602,"timestamp-ms":1775647694517}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/profile_warehouse/tracking/match_metadata/metadata/00000-4a6aa9a1-65e0-450f-9be7-7684faeeac52.metadata.json","timestamp-ms":1775647690681}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":2219873109471251602,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":1} diff --git a/profile_warehouse/tracking/match_metadata/metadata/e27f5ba5-48f7-4cb2-ae56-16a5fe7fd851-m0.avro b/profile_warehouse/tracking/match_metadata/metadata/e27f5ba5-48f7-4cb2-ae56-16a5fe7fd851-m0.avro new file mode 100644 index 0000000000000000000000000000000000000000..2b7d9b67e3ec2fbc252e031cebfe25166969d565 GIT binary patch literal 4470 zcmb`KO>Epm6vx}7stSa_0RoW)W>K4J&uno=u#1ZDWrU z(kh3fH-z{Q&|WxF5JE*Nq8DzI2q=WK3eX6N3sR*5=bnHAZ^p6L-c9q>m7*xIJ^%OS z_de!Lu{^!Mz0KFBNEQEg;JiTRT_Ow$KJ@~f3ZhUkiDmf0;KX^sG=xD}6l`2&UsN&G zr+VO-m{?=llkmSsUJFc*z~`Vs7Q}F=5--)As!OOzO~>9&pn@$wR#0OFpJ0ENi?i|K zDxuiG6l>FRt_;gJNWz1hqs=KLWBUb?s&i- z#X&+4r7%1QB8X|=$8WjwNP>^U2qVF?sjwiT#0nd`{**&C7Ss*VaG6|NHX+!7WwG~` z1HmrAAK)_%EGi5l_9`t2nLuovXm(I76R%EI-K%WHw20?X0V=-wy>w+$EYsV2_Ng0)a zCrb+3PDJ?Ss6-J9G@^lP5i#60MrG_10|62`MHuU_m=P=HG?K9NG~EfHh(J8*0}w-5 zN~CpIBsJ^fybO?2L7{?=%+MHPoHoI!`{p^KnYLD*r=-4?Wht3Qyc%nUPB|1?h-o0E z!OVmjsq)kvE2!EU`=U|3AJjH_MO^f?%*URG=h@axN~+NQppQh2rPi0CAk4^epEfJY z{V^-HBGIg72-k`fSZf05BvrXD<@qeO0$!ofl4$>_HT?@E7JAE>G<9s9;zt{Tlgkwr zO7J}0xY&cR3Jcg}Aqwdr(?HaL9|n}Og#0i}emK4+_AB}B}}!inc1rL159;b?=aQ=l=nL-e089iFxlk7`T^u+NHy-- zVTVZLwVXNPX=UIJQz4K{HY6q2OteZb%hSHt$gUfK(z`XTuO&#*`yGuZ zY~7o8`Pc~ZD3V8I%PFTr=)0ClHK$VX32i6@H8IP@=4%r~JX{cqRN)#FIn5@kXx<1% zt1Zv*lgtH*%P<5*exwo}E)W>Ln1>5bqlY^z)jAJ*cjw^{yIsSC;iG^}o-k(^gc7f6 z+5~yd9Ev<-^IQD40X8XI6JWUWkJmwWJYen_P&|#7xa?-9B)@=c74iIwFsr&A@!6#p zj%goEz*PiHu!{;z%BIg<2}i1E;`3nW=CO$$RIzPVh)*?;@#ckveTS}{dA+OH{r;0L zbhZ8S)%3S5U$*MyGhh7m>c78VIehasMUpztj{LZizqYo%{#a%_bM$u4kspd9BbkxH z>XGi-bNS5ZPd9poGiyEFBV((pE2D20^J4zZ;_A&~`IVK{H(EcqaPPZIhdVA;?p*5l z>96YjXMS#}~Vn?)`b;q1MN5MG0WPi=v68A%duhcs94DS@rc{@a9i*wKKIP(^4HnLcP}qpS<+jcZhhyQ-C)f`@aAG{!3Hrbl%od zq5+*GFbyI(5DE#=NL{tSM{DYYP*|wtLpyX}K#c|+VfmV=D3t`BF`*{Gpk}HuX!wNk zI5t#3gB{Qy2y4DjG3YSxZVAg!Dv)7Z0Th;|pNN!zDtNHHo1;k`a(n%p0VQn6T$P`T zBdExPy|s388c@@}D$r0I5-RF|CUQY6VHOtVd4{ri2sMz_W`)RDvWOe81SreKfBaa3 zzZ21f|6q$htq-|Wm46it1!`@ps0a=f1v8ChXl4>cmX1htD2XgHyA(UikOPl2x*RTH zjWUy0f`mFRklg?=(*PGZ>;StnRuRiJ+rqUNYCRc%EJIne=VG;*tN#HXM+zjOgiUw( ze2oL|@(gw%LRqs#F9xRvE(RGYkg^iI2%HwU2r>>BxO0sH1ZUITEyz{i!QgG7U4aLI z(?fR(xw$1P$A@>!UMpv9Jd0RbSYQ|+X{zJQ+CqVZrGOl0^$vKH1}ttAev>PO(pRn1HEJmae6~Y^fF8 z2@NsEM$@&wX@vO*mrAXp-42tY3Mr5Q1um-` zM*pXH?mk^O9XZ!qvZ;S&F>kQvPutm_-%Bb!E`CEhhu++3|8#Wh?fKE>-m;a9E)?dTl$GE3U3vTRqj%)_7x5;V`tfUOTja(5l^Op9Ts2xr literal 0 HcmV?d00001 diff --git a/profile_warehouse/tracking/tracking_data/metadata/00000-b3a6aabe-7045-4467-ab11-81db388158b2.metadata.json b/profile_warehouse/tracking/tracking_data/metadata/00000-b3a6aabe-7045-4467-ab11-81db388158b2.metadata.json new file mode 100644 index 0000000..7453fe5 --- /dev/null +++ b/profile_warehouse/tracking/tracking_data/metadata/00000-b3a6aabe-7045-4467-ab11-81db388158b2.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/profile_warehouse/tracking/tracking_data","table-uuid":"e3737ab8-990e-4a3d-9c9d-23e2292ada10","last-updated-ms":1775647690678,"last-column-id":11,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[{"source-id":1,"field-id":1000,"transform":"identity","name":"match/id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"snapshots":[],"snapshot-log":[],"metadata-log":[],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":0} diff --git a/profile_warehouse/tracking/tracking_data/metadata/00001-6f0b9eff-eb36-4de8-8147-cabad48bca68.metadata.json b/profile_warehouse/tracking/tracking_data/metadata/00001-6f0b9eff-eb36-4de8-8147-cabad48bca68.metadata.json new file mode 100644 index 0000000..1c440ba --- /dev/null +++ b/profile_warehouse/tracking/tracking_data/metadata/00001-6f0b9eff-eb36-4de8-8147-cabad48bca68.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/profile_warehouse/tracking/tracking_data","table-uuid":"e3737ab8-990e-4a3d-9c9d-23e2292ada10","last-updated-ms":1775647694509,"last-column-id":13,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false},{"id":12,"name":"name","type":"string","required":false},{"id":13,"name":"entity_key","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":1,"field-id":1000,"transform":"identity","name":"match/id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":3010651843225263066,"snapshots":[{"snapshot-id":3010651843225263066,"sequence-number":1,"timestamp-ms":1775647694509,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/profile_warehouse/tracking/tracking_data/metadata/snap-3010651843225263066-0-f03266b5-4cd0-499d-b61d-580fce440e8d.avro","summary":{"operation":"append","added-files-size":"69406005","added-data-files":"1","added-records":"3650000","changed-partition-count":"1","total-data-files":"1","total-delete-files":"0","total-records":"3650000","total-files-size":"69406005","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":3010651843225263066,"timestamp-ms":1775647694509}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/profile_warehouse/tracking/tracking_data/metadata/00000-b3a6aabe-7045-4467-ab11-81db388158b2.metadata.json","timestamp-ms":1775647690678}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":3010651843225263066,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":1} diff --git a/profile_warehouse/tracking/tracking_data/metadata/f03266b5-4cd0-499d-b61d-580fce440e8d-m0.avro b/profile_warehouse/tracking/tracking_data/metadata/f03266b5-4cd0-499d-b61d-580fce440e8d-m0.avro new file mode 100644 index 0000000000000000000000000000000000000000..e64c3fc0fa37b43a91ca4a668abfb754f09740e0 GIT binary patch literal 5402 zcmb_gYitx%6b9PbidrEOELh`ZM0vQ|?#x18YJvtUEstX5jdi^{bEiA)?96uNZecYA zu|6sqDWE(ALsLZ!KTNC9_()8xHNI+nC1{8-`~ZK55+o!7M)cg7&g{;%)V6NYH0|u1 z$NkQCzH{!@gc~O2Uu}vJ62*^Q^9V;1F~U`FCN<(B<$Rp15LGg{ij9wOio{j$K2F0? z_C^#_F;WcAd?_}HRuBIhq$#c#1YVc1YLXV8Z7aC8q8^PAs!&DO#^fRBdN2t#$7sC3 zHKt0Gm=s52$=p*PT_*j7uDxsk1fkTUtNKU>FL5nn#8C7xKvtTxRB?hBV?wLUHKqhd z$7EXWnr=>FB&#}Rl(-B&e84qIjNT|osE0>ylw^`c#!)knPZEe|z`sPOk)-4bq#vHZ zbw1AS5dUBgC=#=W-Z1=DgTQmvBXuo>F>Khx_M0&xK0guq#?<46NE}n=edwh?03f3V z)=XJ9qR>;eTgp;O@3NLU&g&ZdO$rn;Lrne6#4w?mCJEDUv!NG>xY~wPpzY%0K9Cg1>zKqaxFfT zVxi%f8PTcLC4)|FGd^jyk<;R;%G_H0QwM)Q&N!}8t^%=-ye>gih$fPd7LSIBk(Nzx zyl!PhB_VS}-wVd;n;{-z0n(Bck`6~(g25uDIpTvUxmi_U80sS6$5ldHWb*^DLRKJF zdmqvbk^MqICjd09ZWbCbOe5)mfU3!IJB{MTTBeJbsX1C*TZJ|$G=eJcM;6P7mlu|Q zJEj%}s z>8s=n9m0HOO{3VKuhZ2r)JCmcnlN zkf_I@dBJ&7m=GUGX(zimlhIdL?T2(3*&a-!*uhwBy&o65s5EVWQLK}ouX@PSnR z+Edg$<|>_H6)_)(`CwseA0Al{bu}K^c(cc z5wxTpIz?#Y`O;7`&zHGaF^d$Dv_^>=&S}O}RQXa*=FM1*Tgu=BbKz{tQ&i&;=#@3J5Y{BL0H24QnHU87$2tn87jtGgv-;FoR{D>_^r3!SYOk z$u=gY2f#}kRadobh48|w>YIoW3Ip!2)&i2Lf+Q@=B(y>a<7ufIWXzf>l#C4Ruv14U z%^bECR>;E23t~J5x4p}On9;!`Ff|SS4>cA3LrsSTF_=L)wXhlvL>Q`}D501wIxa~D zFA~9&h0&evoSkbOko0n+@g7Yy5;1FC1^38tkHV@R_Lk6$sR|A0vTPDMq!EHCJER*v zxk0$g1yM;9u3t{lY_oFq4STg3agN{5QlK~tOOTU?hwyN5!SF@`E?Pq~9A&GfWmqz{ z3@h0;M6h9)D9$!d*fS(Ti4j#af((5Vax!H5oBOu}F)3V6V7W7k(m>Y@z}%3ad4_J< z3x=DL)~+>D#j6&>t{O9l$!_g1rcDR|S0D(%E=aH`E2fo-y;3b!L8PfBu(S8s2~5Xl>44)VLd#AtFySlUC+uZBkd-|5oXB?^2_Ro9gy(ceTfjSyH z4#*FwlDtPc(zxqjxT7(w?C(4v$@@AR8*uxcgAL(ud&ADowe9Oxt$T4p@AbzErWO1; zv$tLJJXO;C##VF7TW9;jCqMXTR@dXpBdh;f zf7i|%ifgXgexq{d&2zu~q$u9@%*5AoUwJ8Kao4uI{3##haGMup-_$l`!qmIb-Pw!V zw&h(pWmXQiNDiPGJw2cFeBARX>N$I$_>#}wieAZn!>FSj|IrV=8 zW}NMwJ#%tw_MSOwCO7>ybwgs&;~!u8{jscY;XiYV?nJZJwC`TGf5Wsxd-olfe@lBL zvNEeSuk=h$b9X^y*S1S*mgQwV^KAC{?X}-O+O%(eU-#)BAIP2h+@BZjE$l1%x%j~o rk2buzWaoX?teW@D;lje(k2O#BAG^5z@TtEym>o?MZaw{GdNKb6lug*6 literal 0 HcmV?d00001 diff --git a/profile_warehouse/tracking/tracking_data/metadata/snap-3010651843225263066-0-f03266b5-4cd0-499d-b61d-580fce440e8d.avro b/profile_warehouse/tracking/tracking_data/metadata/snap-3010651843225263066-0-f03266b5-4cd0-499d-b61d-580fce440e8d.avro new file mode 100644 index 0000000000000000000000000000000000000000..da5df86c17c36626620d8207c644478ec89c2e4f GIT binary patch literal 1829 zcmb7EO-K}B7}kwQETtc@gpzTPh}g}U{mb;0k(7a?8HnWc?aa6C)ciT$H*SkAQ3w$p zB0J?FLNx4<)lIy)f@D%q_ID^L3h9t^@nQ$-o86stbab@q_r1^ayzle=G)AuE?!Q+Oh6pNe5}#9ffJrt~OVb%D zF5(zs{vf50S<7;26kpFizL@<3pZj2yD|VlfC6XD#dNjaF_g^>UWs;JZ*%&As30at7&xr5 z8U3H)x%+Hx5}nLj&aYkS55Ang^=sSQbXCzq&s$GRUqfMvInZ0TSi?SkG;Aysy)&l2 zKlLStDxbGcG!&E%j-fBZ^_>eRpBWV|7^bHOO}3t&7-$-gUp^#1xx3dp^0{{WM)A?v zo5OR3Ev2t2-lzI5+@Iu*{X6wqFs literal 0 HcmV?d00001 diff --git a/pyproject.toml b/pyproject.toml index a1a72dc..07d69e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,10 @@ all = [ "pandas>=1.4.4", "polars>=0.16.14", "pyarrow>=8.0.0", + "pyiceberg>=0.8.1", + "sqlalchemy>=2.0.0", ] +catalog = ["pyiceberg>=0.8.1", "sqlalchemy>=2.0.0"] pandas = ["pandas>=1.4.4", "pyarrow>=8.0.0"] polars = ["polars>=0.16.14"] pyarrow = ["pyarrow>=8.0.0"] @@ -39,6 +42,8 @@ dev = [ "polars>=0.16.14", "pre-commit>=4.3.0", "pyarrow>=8.0.0", + "pyiceberg>=0.8.1", + "sqlalchemy>=2.0.0", ] all = [ { include-group = "benchmarks" }, diff --git a/python/tests/benchmarks/test_catalog_performance.py b/python/tests/benchmarks/test_catalog_performance.py new file mode 100644 index 0000000..65f7c40 --- /dev/null +++ b/python/tests/benchmarks/test_catalog_performance.py @@ -0,0 +1,68 @@ +import numpy as np +import pytest +import tracking_data_container as tdc + + +@pytest.fixture +def performance_catalog(tmp_path): + warehouse_path = tmp_path / "perf_warehouse" + catalog = tdc.TrackingDataCatalog(str(warehouse_path)) + catalog.initialize() + + # Generate 100k frames for 25 entities + num_frames = 100_000 + entities = ["ball"] + [f"p{i}" for i in range(22)] + ["ref1", "ref2"] + + data = { + "match/id": ["match_1"] * num_frames, + "timestamp": [i * 40000 for i in range(num_frames)], + "frame_id": list(range(num_frames)), + "period": ["1"] * num_frames, + } + + for ent in entities: + if ent == "ball": + prefix = "ball/" + else: + prefix = f"teams/home/players/{ent}/" if ent.startswith("p") else f"referees/{ent}/" + + data[f"{prefix}x"] = np.random.randn(num_frames).tolist() + data[f"{prefix}y"] = np.random.randn(num_frames).tolist() + + container = tdc.TrackingDataContainer(data, validate=False) + catalog.save_container(container) + return catalog + + +def test_benchmark_catalog_load_lazy(performance_catalog, benchmark): + """Benchmark the initialization of a lazy container from catalog.""" + + def load(): + return performance_catalog.load_container(match_id="match_1") + + lazy_c = benchmark(load) + assert isinstance(lazy_c, tdc.LazyTrackingDataContainer) + + +def test_benchmark_catalog_collect_full(performance_catalog, benchmark): + """Benchmark the full collection (pivot) of a standard match.""" + lazy_c = performance_catalog.load_container(match_id="match_1") + + def collect_full(): + return lazy_c.collect() + + container = benchmark(collect_full) + assert container.num_rows() == 100_000 + + +def test_benchmark_catalog_collect_selection(performance_catalog, benchmark): + """Benchmark targeted selection collect (Selection Pushdown).""" + lazy_c = performance_catalog.load_container(match_id="match_1") + + def collect_selection(): + # Only pivot the ball - should be very fast + return lazy_c.select(["ball/x", "ball/y"]).collect() + + container = benchmark(collect_selection) + assert container.num_rows() == 100_000 + assert len(container.schema().names()) == 2 diff --git a/python/tests/test_advanced_features.py b/python/tests/test_advanced_features.py new file mode 100644 index 0000000..507ef4f --- /dev/null +++ b/python/tests/test_advanced_features.py @@ -0,0 +1,98 @@ +from datetime import datetime, timedelta, timezone + +import polars as pl +import tracking_data_container as tdc + + +def test_materialize_frame_temporal(container): + """Test materializing frames by datetime and timedelta.""" + base_time = datetime(2023, 10, 1, 12, 0, 0, tzinfo=timezone.utc) + + # 1. By datetime + # Frame 1 timestamp is base_time + 40ms = 1696161600040000 microseconds + dt = base_time + timedelta(milliseconds=40) + frame_dt = container.materialize_frame(dt) + assert frame_dt["frame_id"] == 123457 + + # 2. By timedelta + # 40ms offset should be Frame 1. + td = timedelta(milliseconds=40) + frame_td = container.materialize_frame(td) + assert frame_td["frame_id"] == 123457 + + +def test_melt_pivot_roundtrip(): + """Test that melting and pivoting correctly reconstructs the wide format.""" + data = { + "match/id": ["m1", "m1"], + "timestamp": [1000000, 1040000], + "frame_id": [1, 2], + "period": ["1", "1"], + "ball/x": [10.0, 11.0], + "ball/y": [20.0, 21.0], + "teams/home/players/p1/x": [30.0, 31.0], + } + container = tdc.TrackingDataContainer(data, validate=False) + + # Melt to long format + long_df = container.melt() + assert "player/id" in long_df.columns + assert "entity_key" in long_df.columns + + # Pivot back to wide + pivoted = tdc.TrackingDataContainer.pivot(long_df) + + assert pivoted.num_rows() == 2 + assert "ball/x" in pivoted.schema().names() + assert "teams/home/players/p1/x" in pivoted.schema().names() + + # Check values + df_piv = pivoted.to_polars().sort("frame_id") + assert df_piv["ball/x"].to_list() == [10.0, 11.0] + assert df_piv["teams/home/players/p1/x"].to_list() == [30.0, 31.0] + + +def test_lazy_schema_caching(container): + """Test that lazy schema caching works and propagates.""" + lazy = container.lazy() + + # First call reconstructions schema + lazy.schema() + assert lazy._cached_schema is not None + + # Transformation should propagate/subset cache + selected = lazy.select(["ball/x", "ball/y"]) + assert selected._cached_schema is not None + assert len(selected._cached_schema.names()) == 2 + + # Slice should preserve cache + sliced = selected.slice(0, 1) + assert sliced._cached_schema == selected._cached_schema + + +def test_pivot_global_constants(): + """Test that global constants are correctly handled in pivot.""" + data = { + "match/id": ["constant_match"] * 4, + "competition/id": ["constant_comp"] * 4, + "timestamp": [1000, 1000, 2000, 2000], + "player/id": ["p1", "p2", "p1", "p2"], + "team/id": ["home", "home", "home", "home"], + "entity_key": ["p1|home", "p2|home", "p1|home", "p2|home"], + "x": [1.0, 2.0, 3.0, 4.0], + } + df = pl.DataFrame(data) + + pivoted = tdc.TrackingDataContainer.pivot(df) + + # Check that constants are back + res = pivoted.to_polars() + assert "match/id" in res.columns + assert res["match/id"][0] == "constant_match" + assert "competition/id" in res.columns + assert res["competition/id"][0] == "constant_comp" + + # Check pivoted columns + assert "teams/home/players/p1/x" in res.columns + assert "teams/home/players/p2/x" in res.columns + assert res.sort("timestamp")["teams/home/players/p1/x"].to_list() == [1.0, 3.0] diff --git a/python/tests/test_catalog.py b/python/tests/test_catalog.py new file mode 100644 index 0000000..28bbf8e --- /dev/null +++ b/python/tests/test_catalog.py @@ -0,0 +1,71 @@ +import tracking_data_container as tdc + + +def test_catalog_lifecycle(tmp_path): + warehouse_path = tmp_path / "warehouse" + catalog = tdc.TrackingDataCatalog(str(warehouse_path)) + catalog.initialize() + + # Create a container + data = { + "match/id": ["match_1", "match_1"], + "timestamp": [1000000, 1040000], # microseconds + "ball/player_id": ["7", "7"], + "ball/team_id": ["home", "home"], + "ball/x": [0.0, 1.0], + "ball/y": [0.0, 1.0], + } + container = tdc.TrackingDataContainer(data, validate=False) + + # Save container + catalog.save_container(container) + + # Load container + lazy_loaded = catalog.load_container(match_id="match_1") + assert isinstance(lazy_loaded, tdc.LazyTrackingDataContainer) + + loaded = lazy_loaded.collect() + assert loaded.num_rows() == 2 + assert loaded.is_bound() + + # Check data + df = loaded.to_polars() + assert df["match/id"][0] == "match_1" + assert df["ball/player_id"][0] == "7" + assert df["ball/x"][0] == 0.0 + assert df["ball/x"][1] == 1.0 + + +def test_bound_container_save(tmp_path): + warehouse_path = tmp_path / "warehouse" + catalog = tdc.TrackingDataCatalog(str(warehouse_path)) + catalog.initialize() + + # Create and save + data = { + "match/id": ["match_1"], + "timestamp": [1000000], + "ball/player_id": ["7"], + "ball/team_id": ["home"], + "ball/x": [0.0], + "ball/y": [0.0], + } + container = tdc.TrackingDataContainer(data, validate=False) + catalog.save_container(container) + + # Load (becomes bound) + lazy_bound = catalog.load_container(match_id="match_1") + bound = lazy_bound.collect() + assert bound.is_bound() + + # Modify and save via bound method + # Preserves catalog binding + new_bound = bound.add_column("ball/z", [1.0]) + assert new_bound.is_bound() + + new_bound.save() + + # Load again to verify + final = catalog.load_container(match_id="match_1").collect() + assert "ball/z" in final.to_polars().columns + assert final.to_polars()["ball/z"][0] == 1.0 diff --git a/python/tests/test_ops.py b/python/tests/test_ops.py index 48bd51a..b4ef388 100644 --- a/python/tests/test_ops.py +++ b/python/tests/test_ops.py @@ -9,7 +9,7 @@ def test_compute_derivatives(): "ball/x": [0.0, 1.0, 3.0, 6.0], "ball/y": [0.0, 0.0, 0.0, 0.0], } - container = tdc.TrackingDataContainer(data) + container = tdc.TrackingDataContainer(data, validate=False) df = container.to_polars() # 1. Speed (dt=1.0) diff --git a/python/tests/test_select.py b/python/tests/test_select.py index ec7be6b..36f22a4 100644 --- a/python/tests/test_select.py +++ b/python/tests/test_select.py @@ -27,7 +27,7 @@ def test_container_select_selector_players(self, container) -> None: "teams/home/players/1/y": [3.0], "teams/away/players/10/x": [4.0], } - c = tdc.TrackingDataContainer(data) + c = tdc.TrackingDataContainer(data, validate=False) subset = c.select(c.selector.players(team_id="home")) assert subset.schema() == tdc.Fields(["teams/home/players/1/x", "teams/home/players/1/y"]) @@ -42,7 +42,7 @@ def test_container_select_selector_player(self) -> None: "teams/home/players/2/x": [4.0], "teams/away/players/1/x": [5.0], } - c = tdc.TrackingDataContainer(data) + c = tdc.TrackingDataContainer(data, validate=False) # Select specific player by ID and team subset = c.select(c.selector.player("1", team_id="home")) @@ -64,7 +64,7 @@ def test_container_select_wildcard(self) -> None: "teams/away/players/10/x": [5.0], "teams/away/players/10/y": [5.1], } - c = tdc.TrackingDataContainer(data) + c = tdc.TrackingDataContainer(data, validate=False) # 1. Select all x coordinates of the home team using ** subset = c.select("teams/home/**/x") diff --git a/python/tests/test_transform.py b/python/tests/test_transform.py index 718900f..3a270e6 100644 --- a/python/tests/test_transform.py +++ b/python/tests/test_transform.py @@ -18,7 +18,9 @@ def test_transform_coordinates(self) -> None: y_direction="up", ) - container = tdc.TrackingDataContainer(data).with_coordinate_system(source_sys) + container = tdc.TrackingDataContainer(data, validate=False).with_coordinate_system( + source_sys + ) # Define target system (Metric -50 to 50) target_sys = tdc.CoordinateSystem( @@ -45,7 +47,9 @@ def test_transform_units(self) -> None: data = {"ball/x": [1.0], "ball/y": [1.0]} # 1 meter source_sys = tdc.CoordinateSystem(length_unit="meters", x_range=(0, 100), y_range=(0, 100)) - container = tdc.TrackingDataContainer(data).with_coordinate_system(source_sys) + container = tdc.TrackingDataContainer(data, validate=False).with_coordinate_system( + source_sys + ) target_sys = tdc.CoordinateSystem(length_unit="yards", x_range=(0, 100), y_range=(0, 100)) transformed = container.transform_coordinates(target_sys) @@ -56,7 +60,7 @@ def test_transform_units(self) -> None: def test_transform_missing_units_raises(self) -> None: data = {"ball/x": [1.0]} - container = tdc.TrackingDataContainer(data) # No metadata + container = tdc.TrackingDataContainer(data, validate=False) # No metadata target_sys = tdc.CoordinateSystem(length_unit="yards") @@ -66,7 +70,9 @@ def test_transform_missing_units_raises(self) -> None: def test_transform_undefined_ranges_raises(self) -> None: # Define system with origin only, no units or ranges sys = tdc.CoordinateSystem(origin="center") - container = tdc.TrackingDataContainer({"ball/x": [1.0]}).with_coordinate_system(sys) + container = tdc.TrackingDataContainer( + {"ball/x": [1.0]}, validate=False + ).with_coordinate_system(sys) target_sys = tdc.CoordinateSystem(length_unit="yards") @@ -81,7 +87,7 @@ def test_transform_orientation(self) -> None: } # Source is static, we want to normalize it - container = tdc.TrackingDataContainer(data, orientation="static_home_away") + container = tdc.TrackingDataContainer(data, validate=False, orientation="static_home_away") # In "ball_owning_team" mode, if "away" has possession, we flip transformed = container.transform_orientation("ball_owning_team") @@ -96,7 +102,7 @@ def test_transform_orientation_home_away(self) -> None: "ball/x": [10.0, 10.0], "period": ["first_half", "second_half"], } - container = tdc.TrackingDataContainer(data, orientation="static_home_away") + container = tdc.TrackingDataContainer(data, validate=False, orientation="static_home_away") # HomeAway: second_half flips transformed = container.transform_orientation("home_away") @@ -107,7 +113,7 @@ def test_transform_orientation_away_home(self) -> None: "ball/x": [10.0, 10.0], "period": ["first_half", "second_half"], } - container = tdc.TrackingDataContainer(data, orientation="static_home_away") + container = tdc.TrackingDataContainer(data, validate=False, orientation="static_home_away") # AwayHome: first_half flips transformed = container.transform_orientation("away_home") @@ -118,7 +124,9 @@ def test_transform_orientation_identity(self) -> None: "ball/x": [10.0, 10.0], "period": ["first_half", "second_half"], } - container = tdc.TrackingDataContainer(data).with_orientation(tdc.OrientationMode.AwayHome) + container = tdc.TrackingDataContainer(data, validate=False).with_orientation( + tdc.OrientationMode.AwayHome + ) # Transforming to the same mode should be a no-op transformed = container.transform_orientation(tdc.OrientationMode.AwayHome) @@ -131,7 +139,7 @@ def test_transform_orientation_static_to_dynamic(self) -> None: "period": ["first_half", "second_half"], } # Data is already in AwayHome orientation but marked as StaticHomeAway - container = tdc.TrackingDataContainer(data).with_orientation( + container = tdc.TrackingDataContainer(data, validate=False).with_orientation( tdc.OrientationMode.StaticHomeAway ) @@ -148,7 +156,9 @@ def test_transform_orientation_dynamic_to_static(self) -> None: "period": ["first_half", "second_half"], } # Data is in AwayHome orientation - container = tdc.TrackingDataContainer(data).with_orientation(tdc.OrientationMode.AwayHome) + container = tdc.TrackingDataContainer(data, validate=False).with_orientation( + tdc.OrientationMode.AwayHome + ) # Transform to StaticHomeAway (which never flips) # first_half: should_flip(AwayHome)=True, should_flip(StaticHomeAway)=False -> Flip @@ -167,7 +177,7 @@ def test_transform_orientation_with_coordinate_system(self) -> None: origin=tdc.Origin.TopLeft, ) container = ( - tdc.TrackingDataContainer(data) + tdc.TrackingDataContainer(data, validate=False) .with_coordinate_system(raw_cs) .with_orientation(tdc.OrientationMode.AwayHome) ) diff --git a/python/tracking_data_container/__init__.py b/python/tracking_data_container/__init__.py index aeb2db0..08e0f81 100644 --- a/python/tracking_data_container/__init__.py +++ b/python/tracking_data_container/__init__.py @@ -1,5 +1,8 @@ from __future__ import annotations +import logging +import warnings + from . import exceptions from ._tracking_data_container import ( Attr, @@ -15,6 +18,8 @@ ops, ) from .builder import TrackingDataBuilder +from .catalog import TrackingDataCatalog +from .config import IcebergConfig from .container import LazyTrackingDataContainer, TrackingDataContainer from .coordinate_system import CoordinateSystem from .io import load_hawkeye, load_sportec, load_statsperform, scan_parquet @@ -31,11 +36,18 @@ ) from .selector import ColumnSelector, Selection +# Silence noisy PyIceberg warnings and logs +warnings.filterwarnings("ignore", category=UserWarning, module="pyiceberg.*") +warnings.filterwarnings("ignore", message=".*Delete operation did not match any records.*") +logging.getLogger("pyiceberg").setLevel(logging.ERROR) + __all__ = ( "__version__", "TrackingDataContainer", "LazyTrackingDataContainer", "TrackingDataBuilder", + "TrackingDataCatalog", + "IcebergConfig", "CoordinateSystem", "Origin", "Direction", diff --git a/python/tracking_data_container/_tracking_data_container.pyi b/python/tracking_data_container/_tracking_data_container.pyi index c9a95f3..113da2b 100644 --- a/python/tracking_data_container/_tracking_data_container.pyi +++ b/python/tracking_data_container/_tracking_data_container.pyi @@ -138,17 +138,10 @@ class _CoordinateSystem: def length_unit(self) -> str | None: ... class Selection: - """A selection of columns based on metadata.""" - - _inner: _Selection - def attributes(self, names: list[str]) -> Selection: ... - @property - def expr(self) -> str: ... - -class _Selection: """Internal selection.""" @property def expr(self) -> str: ... + def attributes(self, names: list[str]) -> Selection: ... class ColumnSelector: """A selector for building column expressions based on metadata.""" @@ -172,9 +165,18 @@ class _TrackingDataContainer: ) -> None: ... @classmethod def _from_capsule(cls, capsule: Any) -> _TrackingDataContainer: ... + @staticmethod + def pivot(data: Any) -> _TrackingDataContainer: ... + @staticmethod + def from_arrow( + array_capsule: Any, + schema_capsule: Any, + coordinate_system: _CoordinateSystem | None = None, + orientation: str | None = None, + ) -> _TrackingDataContainer: ... def lazy(self) -> _LazyTrackingDataContainer: ... def select( - self, items: str | list[str | _DataField | _Selection] | _DataField | _Selection + self, items: str | list[str | _DataField | Selection] | _DataField | Selection ) -> _TrackingDataContainer: ... def add_column( self, name: str, value: Any, _schema_capsule: Any = None @@ -209,7 +211,7 @@ class _LazyTrackingDataContainer: """Lazy tracking data container (internal).""" def collect(self) -> _TrackingDataContainer: ... def select( - self, items: str | list[str | _DataField | _Selection] | _DataField | _Selection + self, items: str | list[str | _DataField | Selection] | _DataField | Selection ) -> _LazyTrackingDataContainer: ... def slice(self, offset: int, length: int) -> _LazyTrackingDataContainer: ... def filter(self, predicate: Expr) -> _LazyTrackingDataContainer: ... @@ -251,6 +253,8 @@ class Expr: def __sub__(self, other: Expr | float | int) -> Expr: ... def __mul__(self, other: Expr | float | int) -> Expr: ... def __truediv__(self, other: Expr | float | int) -> Expr: ... + def __and__(self, other: Expr) -> Expr: ... + def __or__(self, other: Expr) -> Expr: ... def eq(self, other: Expr | Any) -> Expr: ... def ne(self, other: Expr | Any) -> Expr: ... def gt(self, other: Expr | Any) -> Expr: ... diff --git a/python/tracking_data_container/catalog.py b/python/tracking_data_container/catalog.py new file mode 100644 index 0000000..bbf9118 --- /dev/null +++ b/python/tracking_data_container/catalog.py @@ -0,0 +1,543 @@ +"""TrackingDataCatalog - Manages container storage and lifecycle.""" + +from __future__ import annotations + +import json +from collections.abc import Sequence +from pathlib import Path +from typing import TYPE_CHECKING, Any, cast + +import polars as pl +import pyarrow as pa +from pyiceberg.catalog import load_catalog +from pyiceberg.exceptions import NoSuchTableError +from pyiceberg.expressions import ( + And, + BooleanExpression, + EqualTo, + Or, + Reference, +) +from pyiceberg.io.pyarrow import _pyarrow_to_schema_without_ids +from pyiceberg.partitioning import PartitionField, PartitionSpec +from pyiceberg.schema import Schema +from pyiceberg.table import Table +from pyiceberg.transforms import IdentityTransform +from pyiceberg.types import ( + FloatType, + LongType, + NestedField, + StringType, + TimestampType, +) + +from .config import IcebergConfig +from .container import CORE_COLUMNS, LazyTrackingDataContainer, TrackingDataContainer +from .schema import DataField, Fields, TrackedEntity + +if TYPE_CHECKING: + pass + + +class TrackingDataCatalog: + """ + Manages storage and lifecycle of TrackingDataContainer instances. + + Backed by Apache Iceberg tables with configurable optimization settings. + Follows Iceberg's Table↔Catalog pattern where containers hold a + reference to their parent catalog. + """ + + def __init__( + self, + warehouse_path: str, + config: IcebergConfig | None = None, + catalog_type: str = "sql", + catalog_properties: dict | None = None, + ): + """ + Initialize catalog. + + Args: + warehouse_path: Path to Iceberg warehouse + config: Iceberg configuration (partitioning, compression, etc.) + catalog_type: Catalog backend (sql, glue, rest, etc.) + catalog_properties: Additional catalog properties + """ + self.warehouse_path = Path(warehouse_path).absolute() + self.config = config or IcebergConfig() + self.catalog_type = catalog_type + + # Create warehouse directory if it doesn't exist + self.warehouse_path.mkdir(parents=True, exist_ok=True) + + # Default properties for SQL catalog (SQLite for POC) + import multiprocessing + + # Optimize IO parallelism + num_workers = multiprocessing.cpu_count() + + default_properties = { + "uri": f"sqlite:///{self.warehouse_path / 'catalog.db'}", + "warehouse": f"file://{self.warehouse_path}", + "py-io-impl": "pyiceberg.io.pyarrow.PyArrowFileIO", + "max-workers": str(num_workers), + } + + self.catalog_properties = catalog_properties or default_properties + + # Load Iceberg catalog + self._iceberg_catalog = load_catalog( + "default", + **{ + "type": self.catalog_type, + **self.catalog_properties, + }, + ) + + self._tracking_table: Table | None = None + self._metadata_table: Table | None = None + + def initialize(self) -> None: + """ + Initialize Iceberg tables (tracking_data + match_metadata). + + Creates tables with configured partitioning and optimization settings. + Must be called once before first use. Idempotent - safe to call multiple times. + """ + # Create namespace if it doesn't exist + try: + self._iceberg_catalog.create_namespace("tracking") + except Exception: + pass # Namespace already exists + + # Create tracking_data table + self._create_tracking_table() + + # Create match_metadata table + self._create_metadata_table() + + def _create_tracking_table(self) -> None: + """Create tracking_data Iceberg table in the requested long format.""" + table_name = "tracking.tracking_data" + + # Check if table exists + try: + self._tracking_table = self._iceberg_catalog.load_table(table_name) + return # Table exists + except NoSuchTableError: + pass # Create new table + + # Define the requested schema with flat names. All fields are optional to allow for + # flexible ingestion and schema evolution. + schema = Schema( + NestedField(1, "match/id", StringType(), required=False), + NestedField(2, "competition/id", StringType(), required=False), + NestedField(3, "season/id", StringType(), required=False), + NestedField(4, "timestamp", TimestampType(), required=False), + NestedField(5, "frame_id", LongType(), required=False), + NestedField(6, "period", StringType(), required=False), + NestedField(7, "player/id", StringType(), required=False), + NestedField(8, "team/id", StringType(), required=False), + NestedField(9, "x", FloatType(), required=False), + NestedField(10, "y", FloatType(), required=False), + NestedField(11, "z", FloatType(), required=False), + ) + + # Define partitioning by match/id + partition_spec = PartitionSpec( + PartitionField( + source_id=1, field_id=1000, transform=IdentityTransform(), name="match/id" + ) + ) + + # Create table + self._tracking_table = self._iceberg_catalog.create_table( + table_name, + schema=schema, + partition_spec=partition_spec, + ) + + def _create_metadata_table(self) -> None: + """Create match_metadata Iceberg table.""" + table_name = "tracking.match_metadata" + + # Check if table exists + try: + self._metadata_table = self._iceberg_catalog.load_table(table_name) + return + except NoSuchTableError: + pass + + # Define schema for metadata as requested + schema = Schema( + NestedField(1, "match/id", StringType(), required=True), + NestedField(2, "competition/id", StringType(), required=False), + NestedField(3, "season/id", StringType(), required=False), + NestedField(4, "metadata_json", StringType(), required=True), + ) + + self._metadata_table = self._iceberg_catalog.create_table( + table_name, + schema=schema, + ) + + def load_container( + self, + match_id: str, + player_id: str | None = None, + **filters: Any, + ) -> CatalogLazyTrackingDataContainer: + """ + Load container from storage lazily. + + Returns a LazyTrackingDataContainer that wraps the physical long-format + Iceberg table but allows logical wide-format analysis. + """ + # Optimized truly lazy loading: don't call to_arrow() yet! + return CatalogLazyTrackingDataContainer( + self, match_id=match_id, player_id=player_id, **filters + ) + + def save_container( + self, + container: TrackingDataContainer, + match_id: str | None = None, + metadata: dict | None = None, + ) -> None: + """ + Save container to storage (replaces match partition). + + Args: + container: Container to save + match_id: Optional match ID override + metadata: Optional additional metadata to save + """ + if match_id is None: + # Try to get match_id from first row if possible + df = container.to_polars().head(1) + if "match/id" in df.columns: + match_id = str(df["match/id"][0]) + elif "match_id" in df.columns: + match_id = str(df["match_id"][0]) + else: + raise ValueError("match_id must be provided if not present in container data") + + # Save tracking data + self._save_tracking_data(container, match_id) + + # Save metadata + self._save_metadata(metadata or {}, match_id) + + def commit_container(self, container: TrackingDataContainer) -> None: + """ + Commit bound container. + + Args: + container: Bound container to commit + """ + # For POC, we just use save_container + # Try to find match_id + df = container.to_polars().head(1) + match_id = df["match/id"][0] if "match/id" in df.columns else df["match_id"][0] + self.save_container(container, match_id=str(match_id)) + + def _save_tracking_data( + self, + container: TrackingDataContainer, + match_id: str, + ) -> None: + """Save tracking data to Iceberg table in entity-long format.""" + if self._tracking_table is None: + self._tracking_table = self._iceberg_catalog.load_table("tracking.tracking_data") + + assert self._tracking_table is not None + + # 1. Melt to entity-long format for stable warehouse storage + long_df = container.melt() + + # 2. Convert to Arrow and handle dictionary casting + arrow_table = long_df.to_arrow() + + # Define known types for core columns to avoid incorrect casting when null + known_types = { + "timestamp": pa.timestamp("us"), + "frame_id": pa.int64(), + "x": pa.float32(), + "y": pa.float32(), + "z": pa.float32(), + } + + # Cast dictionary columns to their value type (usually string), + # null types to string (or known type) for Iceberg compatibility, + # and ensure timestamp is correctly typed. + new_columns = [] + new_fields = [] + for i, field in enumerate(arrow_table.schema): + target_type = None + if field.name in known_types: + target_type = known_types[field.name] + elif isinstance(field.type, pa.DictionaryType): + target_type = field.type.value_type + elif pa.types.is_null(field.type): + target_type = pa.string() + + if target_type is not None: + new_columns.append(arrow_table.column(i).cast(target_type)) + new_fields.append(pa.field(field.name, target_type, field.nullable)) + else: + new_columns.append(arrow_table.column(i)) + new_fields.append(field) + + arrow_table = pa.Table.from_arrays(new_columns, schema=pa.schema(new_fields)) + + # Start transaction for atomic delete + append + schema update + with self._tracking_table.transaction() as txn: + # Update schema to include any new metrics (e.g. speed, acc) + container_schema = _pyarrow_to_schema_without_ids(arrow_table.schema) + txn.update_schema().union_by_name(container_schema).commit() + + # Delete existing match partition + txn.delete(EqualTo(term=Reference("match/id"), value=match_id)) # type: ignore + + # Append long-format data + txn.append(arrow_table) + + def _save_metadata(self, metadata: dict, match_id: str) -> None: + """Save metadata to Iceberg table.""" + if self._metadata_table is None: + self._metadata_table = self._iceberg_catalog.load_table("tracking.match_metadata") + + assert self._metadata_table is not None + + # Define schema for the Arrow table + metadata_schema = pa.schema( + [ + pa.field("match/id", pa.string(), nullable=False), + pa.field("competition/id", pa.string(), nullable=True), + pa.field("season/id", pa.string(), nullable=True), + pa.field("metadata_json", pa.string(), nullable=False), + ] + ) + + # Prepare metadata row + metadata_row = pa.table( + { + "match/id": [match_id], + "competition/id": [metadata.get("competition_id")], + "season/id": [metadata.get("season_id")], + "metadata_json": [json.dumps(metadata)], + }, + schema=metadata_schema, + ) + + # Upsert metadata + with self._metadata_table.transaction() as txn: + txn.delete(EqualTo(term=Reference("match/id"), value=match_id)) # type: ignore + txn.append(metadata_row) + + def __repr__(self) -> str: + return ( + f"TrackingDataCatalog(\n" + f" warehouse={self.warehouse_path},\n" + f" catalog_type={self.catalog_type}\n" + f")" + ) + + +class CatalogLazyTrackingDataContainer(LazyTrackingDataContainer): + """A lazy container that defers loading from an Iceberg catalog.""" + + def __init__( + self, + catalog: TrackingDataCatalog, + match_id: str, + player_id: str | None = None, + selection: str | Sequence[str] | None = None, + limit: int | None = None, + **filters: Any, + ): + # We pass None for the inner Rust container as we manage the scan in collect() + super().__init__(None, catalog=catalog, is_long_storage=True) # type: ignore + self._catalog: TrackingDataCatalog = catalog + self._match_id = match_id + self._player_id = player_id + self._selection = selection + self._limit = limit + self._filters = filters + self._cached_schema: Fields | None = None + + def schema(self) -> Fields: + """Reconstruct the wide schema by inspecting entities in the match.""" + if self._cached_schema: + return self._cached_schema + + assert self._catalog is not None + + # Fast path: just get unique entities for this match + if self._catalog._tracking_table is None: + self._catalog._tracking_table = self._catalog._iceberg_catalog.load_table( + "tracking.tracking_data" + ) + + assert self._catalog._tracking_table is not None + + filter_expr: BooleanExpression = EqualTo(term=Reference("match/id"), value=self._match_id) # type: ignore + if self._player_id: + filter_expr = And( + filter_expr, + EqualTo(term=Reference("player/id"), value=self._player_id), # type: ignore + ) + + # Only scan player/id and team/id to reconstruct schema + entities_table = self._catalog._tracking_table.scan( + row_filter=filter_expr, selected_fields=("player/id", "team/id") + ).to_arrow() + + entities_df = cast(pl.DataFrame, pl.from_arrow(entities_table)) + if isinstance(entities_df, pl.Series): + entities_df = entities_df.to_frame() + entities_df = entities_df.unique() + + # All potential physical attributes (x, y, z, etc.) + physical_names = [f.name for f in self._catalog._tracking_table.schema().fields] + attr_cols = [c for c in physical_names if c not in CORE_COLUMNS and c != "id"] + + # Base list: all core columns that are NOT entity-specific + wide_names = [ + c + for c in physical_names + if c in CORE_COLUMNS and c not in ("player/id", "team/id", "entity_key") + ] + for row in entities_df.to_dicts(): + p_id = str(row.get("player/id")) + t_id = str(row.get("team/id", "none")) + + prefix = TrackedEntity.from_entity_key(f"{p_id}|{t_id}").cdf_prefix() + for attr in attr_cols: + wide_names.append(f"{prefix}{attr}") + + self._cached_schema = Fields(list(dict.fromkeys(wide_names))) + return self._cached_schema + + def select(self, items: str | Sequence[str]) -> CatalogLazyTrackingDataContainer: + """Translate wide selection to long selection pushdown.""" + assert self._catalog is not None + return CatalogLazyTrackingDataContainer( + self._catalog, + self._match_id, + self._player_id, + selection=items, + limit=self._limit, + **self._filters, + ) + + def slice(self, offset: int, length: int) -> CatalogLazyTrackingDataContainer: + """Apply limit to the lazy container.""" + assert self._catalog is not None + return CatalogLazyTrackingDataContainer( + self._catalog, + self._match_id, + self._player_id, + selection=self._selection, + limit=length, + **self._filters, + ) + + def collect(self) -> TrackingDataContainer: + """Finally perform the Iceberg scan and pivot.""" + assert self._catalog is not None + + if self._catalog._tracking_table is None: + self._catalog._tracking_table = self._catalog._iceberg_catalog.load_table( + "tracking.tracking_data" + ) + + assert self._catalog._tracking_table is not None + + filter_expr: BooleanExpression = EqualTo(term=Reference("match/id"), value=self._match_id) # type: ignore + if self._player_id: + filter_expr = And( + filter_expr, + EqualTo(term=Reference("player/id"), value=self._player_id), # type: ignore + ) + + # 1. Determine which long-format columns we need + selected_long_fields = None + deferred_select = None + + if self._selection: + # Reconstruct which long columns are needed for this wide selection + eager_schema = self.schema() + literal_names: list[str] = [] + input_items = ( + self._selection if isinstance(self._selection, (list, tuple)) else [self._selection] + ) + for item in input_items: + if isinstance(item, str): + if "*" in item: + literal_names.extend(eager_schema.match(item)) + else: + literal_names.append(item) + else: + literal_names.append(str(item)) + + literal_names = list(dict.fromkeys(literal_names)) + deferred_select = literal_names + + needed_long = set(CORE_COLUMNS) + needed_ents: set[str] = set() + + for name in literal_names: + if name in CORE_COLUMNS: + needed_long.add(name) + else: + field = DataField(name) + needed_long.add(field.attribute) + if field.entity: + needed_ents.add(cast(str, field.entity_key())) + + # Check physical schema + physical_names = {f.name for f in self._catalog._tracking_table.schema().fields} + selected_long_fields = [f for f in needed_long if f in physical_names] + + # 2. Add entity filter pushdown + if needed_ents: + entity_predicate: BooleanExpression | None = None + + for k in needed_ents: + p_id, t_id = k.split("|") + cond: BooleanExpression = EqualTo(term=Reference("player/id"), value=p_id) # type: ignore + if p_id != "ball" and t_id != "none": + cond = And(cond, EqualTo(term=Reference("team/id"), value=t_id)) # type: ignore + + if entity_predicate is None: + entity_predicate = cond + else: + entity_predicate = Or(entity_predicate, cond) + + if entity_predicate: + filter_expr = And(filter_expr, entity_predicate) + + # 3. Perform the scan + scan_kwargs: dict[str, Any] = {"row_filter": filter_expr} + if selected_long_fields is not None: + scan_kwargs["selected_fields"] = tuple(selected_long_fields) + if self._limit: + # Rough estimate for long format: 50 rows per frame + scan_kwargs["limit"] = self._limit * 50 + + scan = self._catalog._tracking_table.scan(**scan_kwargs) + arrow_table = scan.to_arrow() + + # 4. Pivot to wide format using high-performance Rust implementation + wide_container = TrackingDataContainer.pivot(arrow_table) + wide_container._catalog = self._catalog + + # Apply final selection/slice + if deferred_select: + wide_container = wide_container.select(deferred_select) + if self._limit: + wide_container = wide_container.slice(0, self._limit) + + return wide_container diff --git a/python/tracking_data_container/config.py b/python/tracking_data_container/config.py new file mode 100644 index 0000000..bb7093c --- /dev/null +++ b/python/tracking_data_container/config.py @@ -0,0 +1,25 @@ +"""Configuration for TrackingDataContainer storage.""" + +from dataclasses import dataclass, field + + +@dataclass +class IcebergConfig: + """Configuration for Iceberg table optimization.""" + + # Partitioning + bucket_count: int = 10 + partition_by: list[str] = field(default_factory=lambda: ["match/id"]) + + # Parquet optimization + compression: str = "snappy" # or "zstd" + page_size: int = 1024 * 1024 # 1 MB + row_group_size: int = 128 * 1024 * 1024 # 128 MB + + # Storage features + write_page_index: bool = True + write_statistics: bool = True + use_dictionary: bool = True + + # Sorting + sort_by: list[str] = field(default_factory=lambda: ["player_id", "timestamp"]) diff --git a/python/tracking_data_container/container.py b/python/tracking_data_container/container.py index 7b3a924..d90cd97 100644 --- a/python/tracking_data_container/container.py +++ b/python/tracking_data_container/container.py @@ -1,7 +1,9 @@ from __future__ import annotations +import logging +from collections.abc import Sequence from datetime import datetime, timedelta -from typing import TYPE_CHECKING, Any, Callable, cast +from typing import TYPE_CHECKING, Any, cast import numpy as np import polars as pl @@ -21,1099 +23,646 @@ Selection as _Selection, ) from .coordinate_system import CoordinateSystem -from .schema import Ball, DataField, Fields +from .exceptions import TrackingDataContainerError +from .schema import DataField, Fields, TrackedEntity from .selector import ColumnSelector, Selection if TYPE_CHECKING: from pathlib import Path import pandas as pd - import polars as pl - import pyarrow as pa + + from .catalog import TrackingDataCatalog + +logger = logging.getLogger(__name__) + +# --- Constants for performance --- +CORE_COLUMNS = { + "match/id", + "competition/id", + "season/id", + "timestamp", + "frame_id", + "period", + "period_id", + "player/id", + "team/id", + "entity_key", +} +FLOAT_SUFFIXES = ("/x", "/y", "/z", "/dist", "/vel", "/acc", "/lat", "/long", "/minutes_played") class TrackingDataContainer: - """ - A high-performance, metadata-aware container for sports tracking data. - - TrackingDataContainer acts as the primary eager interface for interacting - with tracking data, providing tools for selection, transformation, and - interoperability with the Python data ecosystem. - - Parameters - ---------- - data : dict or _TrackingDataContainer - The underlying data, either as a dictionary of columns or an existing - internal container. - coordinate_system : CoordinateSystem, optional - Spatial metadata describing the coordinate frame. - orientation : str or OrientationMode, optional - The playing direction normalization mode. - - Examples - -------- - >>> import tracking_data_container as tdc - >>> data = {"ball/x": [0.0, 1.0], "ball/y": [0.0, 1.0]} - >>> container = tdc.TrackingDataContainer(data) - >>> container.num_rows() - 2 - >>> # Select specific columns - >>> ball_pos = container.select(["ball/x", "ball/y"]) - >>> # Convert to other formats - >>> df = container.to_polars() - """ + """A high-performance, metadata-aware container for sports tracking data.""" _container: _TrackingDataContainer + _catalog: TrackingDataCatalog | None def __init__( self, - data: dict[str, Any] | _TrackingDataContainer | pl.DataFrame, + data: dict[str, Any] | _TrackingDataContainer | pl.DataFrame | pa.Table, coordinate_system: CoordinateSystem | None = None, orientation: str | OrientationMode | None = None, + catalog: TrackingDataCatalog | None = None, + validate: bool = True, ) -> None: + self._catalog = catalog if isinstance(data, _TrackingDataContainer): self._container = data + return + + data_dict: dict[str, Any] + if isinstance(data, pl.DataFrame): + data_dict = data.to_dict(as_series=False) + elif isinstance(data, pa.Table): + data_dict = data.to_pydict() else: - if isinstance(data, pl.DataFrame): - data = data.to_dict(as_series=False) - - # Deep normalization of the dictionary - normalized_data = {} - for k, v in data.items(): - if v is None or len(v) == 0: - normalized_data[k] = v - continue + data_dict = data - # Ensure all inputs are native python types for StructArray construction - if isinstance(v, (np.ndarray, pl.Series, pa.Array)): - # Special handling for datetime/timedelta to microseconds - if k == "timestamp": - if isinstance(v, np.ndarray): - if v.dtype.kind == "M": - normalized_data[k] = [ - int(dt.astype("datetime64[us]").view(np.int64)) for dt in v - ] - else: - normalized_data[k] = v.tolist() - elif isinstance(v, pl.Series): - normalized_data[k] = v.dt.timestamp("us").to_list() - else: - normalized_data[k] = v.to_pylist() - # Ensure coordinates are Float32 - elif k.endswith("/x") or k.endswith("/y") or k.endswith("/z"): - normalized_data[k] = [float(x) for x in v] - else: - normalized_data[k] = v.tolist() if hasattr(v, "tolist") else list(v) - elif isinstance(v, list) and len(v) > 0: - if isinstance(v[0], timedelta): - normalized_data[k] = [int(td.total_seconds() * 1_000_000) for td in v] - elif isinstance(v[0], datetime): - normalized_data[k] = [int(dt.timestamp() * 1_000_000) for dt in v] - else: - normalized_data[k] = v - else: - normalized_data[k] = v + normalized = {} + for k, v in data_dict.items(): + if v is None or len(v) == 0: + normalized[k] = v + continue - inner_cs = coordinate_system._inner if coordinate_system else None - orientation_str = str(orientation) if orientation else None + if k.endswith(FLOAT_SUFFIXES) or k in ("x", "y", "z", "ball_x", "ball_y", "ball_z"): + normalized[k] = [float(x) if x is not None else None for x in v] + elif k.endswith("_status") or k == "ball_status": + normalized[k] = [bool(x) if x is not None else None for x in v] + elif k in ("timestamp", "frame_id", "period_id"): + if len(v) > 0 and isinstance(v[0], datetime): + normalized[k] = [ + int(dt.timestamp() * 1_000_000) if dt is not None else None for dt in v + ] + else: + normalized[k] = [int(x) if x is not None else None for x in v] + else: + normalized[k] = [str(x) if x is not None else None for x in v] - self._container = _TrackingDataContainer(normalized_data, inner_cs, orientation_str) + inner_cs = coordinate_system._inner if coordinate_system else None + self._container = _TrackingDataContainer( + normalized, inner_cs, str(orientation) if orientation else None + ) + if validate: + self.validate() - # --- Properties and Metadata --- + def num_rows(self) -> int: + return self._container.num_rows() def schema(self) -> Fields: - """Returns the schema of the container.""" return Fields(self._container.schema) + def validate(self) -> None: + self._container.validate() + + def is_bound(self) -> bool: + return self._catalog is not None + + def save(self) -> None: + if self._catalog is None: + raise ValueError("Container is not bound to a catalog.") + self._catalog.save_container(self) + + @property + def selector(self) -> ColumnSelector: + return ColumnSelector(self._container.selector) + @property def coordinate_system(self) -> CoordinateSystem | None: - """Returns the active coordinate system metadata.""" if self._container.coordinate_system is None: return None return CoordinateSystem._from_inner(self._container.coordinate_system) @property def orientation_mode(self) -> OrientationMode | None: - """Returns the active orientation normalization mode.""" return self._container.orientation_mode - @property - def selector(self) -> ColumnSelector: - """Returns the selector for building column expressions.""" - return ColumnSelector(self._container.selector) - - def num_rows(self) -> int: - """Returns the number of frames in the container.""" - return self._container.num_rows() - - def validate(self) -> None: - """ - Validates the container against the Common Data Format (CDF). - - Raises - ------ - SchemaError - If mandatory columns are missing or have incorrect types. - SchemaWarning - If non-standard columns are present. - """ - self._container.validate() - - def __len__(self) -> int: - return self.num_rows() - - def __getitem__(self, key: int) -> Frame: - """Access a specific frame by index.""" - return self.materialize_frame(key) - - def slice(self, offset: int, length: int) -> TrackingDataContainer: - """ - Extract a slice of frames from the container. - - Parameters - ---------- - offset : int - The starting frame index. - length : int - The number of frames to include. - - Returns - ------- - TrackingDataContainer - A new container containing only the specified frames. - """ - new_inner = self._container.slice(offset, length) - new_container = TrackingDataContainer.__new__(TrackingDataContainer) - new_container._container = new_inner - return new_container - - # --- Transformations --- - def with_coordinate_system(self, system: CoordinateSystem) -> TrackingDataContainer: - """ - Sets the coordinate system metadata without performing any transformations. - - Parameters - ---------- - system : CoordinateSystem - The new coordinate system. - - Returns - ------- - TrackingDataContainer - A new container with updated metadata. - """ new_inner = self._container.with_coordinate_system(system._inner) - return TrackingDataContainer(new_inner) + return TrackingDataContainer(new_inner, catalog=self._catalog) def transform_coordinates(self, to_system: CoordinateSystem) -> TrackingDataContainer: - """ - Transforms coordinates to a new spatial system. - - Parameters - ---------- - to_system : CoordinateSystem - The target coordinate system. - - Returns - ------- - TrackingDataContainer - A new container with transformed coordinates. - """ new_inner = self._container.transform_coordinates(to_system._inner) - return TrackingDataContainer(new_inner) + return TrackingDataContainer(new_inner, catalog=self._catalog) def with_orientation(self, mode: str | OrientationMode) -> TrackingDataContainer: - """ - Sets the orientation metadata without performing any transformations. - - Parameters - ---------- - mode : str or OrientationMode - The new orientation mode. - - Returns - ------- - TrackingDataContainer - A new container with updated metadata. - """ - if not isinstance(mode, str): - mode = str(mode) - new_inner = self._container.with_orientation(mode) - return TrackingDataContainer(new_inner) + new_inner = self._container.with_orientation(str(mode)) + return TrackingDataContainer(new_inner, catalog=self._catalog) def transform_orientation(self, mode: str | OrientationMode) -> TrackingDataContainer: - """ - Normalizes coordinates based on playing direction. - - Parameters - ---------- - mode : str or OrientationMode - The orientation normalization mode. - - Returns - ------- - TrackingDataContainer - A new container with normalized coordinates. - """ - if not isinstance(mode, str): - mode = str(mode) - new_inner = self._container.transform_orientation(mode) - return TrackingDataContainer(new_inner) - - # --- Selections and Data Additions --- - - def select(self, items: str | list[str | DataField | Selection]) -> TrackingDataContainer: - """ - Select columns from the container. - - Parameters - ---------- - items : str or list of str or DataField or Selection - The columns to select. - - Returns - ------- - TrackingDataContainer - A new container with only the selected columns. - """ - processed_items: str | list[str | _DataField | _Selection] | _DataField | _Selection - if isinstance(items, list): - processed_items = [] - for i in items: - if isinstance(i, DataField): - processed_items.append(i._field) - elif isinstance(i, Selection): - processed_items.append(i._selection) + new_inner = self._container.transform_orientation(str(mode)) + return TrackingDataContainer(new_inner, catalog=self._catalog) + + def select(self, items: str | Sequence[str | DataField | Selection]) -> TrackingDataContainer: + literal_names: list[str] = [] + input_items = items if isinstance(items, (list, tuple)) else [items] + for item in input_items: + if isinstance(item, str): + if "*" in item: + literal_names.extend(self.schema().match(item)) else: - processed_items.append(i) - elif isinstance(items, DataField): - processed_items = items._field - elif isinstance(items, Selection): - processed_items = items._selection + literal_names.append(item) + elif isinstance(item, DataField): + literal_names.append(item.name()) + + processed: list[str | _DataField | _Selection] = [] + if literal_names: + processed = [_DataField.from_name(n) for n in literal_names] else: - processed_items = items + for i in input_items: + if isinstance(i, DataField): + processed.append(i._field) + elif isinstance(i, Selection): + processed.append(i._inner) + elif isinstance(i, str): + processed.append(_DataField.from_name(i)) + elif isinstance(i, (_DataField, _Selection)): + processed.append(i) - new_inner = self._container.select(processed_items) # type: ignore - new_container = TrackingDataContainer.__new__(TrackingDataContainer) - new_container._container = new_inner - return new_container + new_inner = self._container.select(processed) + return TrackingDataContainer(new_inner, catalog=self._catalog, validate=False) + + def slice(self, offset: int, length: int) -> TrackingDataContainer: + new_inner = self._container.slice(offset, length) + return TrackingDataContainer(new_inner, catalog=self._catalog, validate=False) def add_column( - self, - name: str, - value: Any | Callable[[pl.DataFrame], Any], - on: str | list[str] | None = None, + self, name: str, value: Any, on: str | Sequence[str] | None = None ) -> TrackingDataContainer: - """ - Add a new column to the container. - - Parameters - ---------- - name : str - The name of the new column. - value : array-like or callable - The data for the new column. If a callable is provided, it will be - invoked with the columns specified in `on` as separate arguments. - on : str or list of str, optional - The columns to pass to the callable. - - Returns - ------- - TrackingDataContainer - A new container including the new column. - """ - import polars as pl - if callable(value): - inputs = self.select(on) if on is not None else self # type: ignore + inputs = self.select(on) if on is not None else self batch = inputs.to_arrow() - if on is not None: - # Pass individual columns as pyarrow arrays - on_list = [on] if isinstance(on, str) else list(on) - args = [batch.column(c) for c in on_list] - data = value(*args) - else: - # Pass the whole record batch - data = value(batch) - - if isinstance(data, pl.DataFrame) and data.width == 1: - data = data.to_series() + args = [batch.column(c) for c in ([on] if isinstance(on, str) else list(on or []))] + data = value(*args) if on else value(batch) elif isinstance(value, _Expr): - # If it's an expression, pass it to the Rust implementation new_inner = self._container.add_column(name, value) - new_container = TrackingDataContainer.__new__(TrackingDataContainer) - new_container._container = new_inner - return new_container + return TrackingDataContainer(new_inner, catalog=self._catalog, validate=False) else: data = value try: - array_capsule, schema_capsule = data.__arrow_c_array__() + cap_arr, cap_sch = data.__arrow_c_array__() except (AttributeError, TypeError): import pyarrow as pa - if isinstance(data, pl.Series): - data = pa.array(data.to_list()) - else: - data = pa.array(data) - array_capsule, schema_capsule = data.__arrow_c_array__() + data = pa.array(data) + cap_arr, cap_sch = data.__arrow_c_array__() + return TrackingDataContainer( + self._container.add_column(name, cap_arr, cap_sch), + catalog=self._catalog, + validate=False, + ) - new_inner = self._container.add_column(name, array_capsule, schema_capsule) - new_container = TrackingDataContainer.__new__(TrackingDataContainer) - new_container._container = new_inner - return new_container - - def add_columns( - self, - names: list[str], - values: list[Any], - on: list[str | list[str] | None] | None = None, - ) -> TrackingDataContainer: - """ - Add multiple new columns to the container in a single operation. - - Parameters - ---------- - names : list of str - The names for the new columns. - values : list of array-like, Expr, or callable - The data or computations for the new columns. - on : list of str or list of list of str, optional - The columns to pass to each callable value. - - Returns - ------- - TrackingDataContainer - A new container including the new columns. - """ - import polars as pl - import pyarrow as pa - - processed_values = [] - schema_capsules: list[Any] = [] - - for i, (name, value) in enumerate(zip(names, values)): - val_on = on[i] if on is not None else None - if callable(value): - # Try to evaluate as expression first if on is provided - data = None - if val_on is not None: - try: - # Attempt to pass string names for Expr building pass - # If val_on is a single string, wrap it in a tuple - on_tuple: tuple[str, ...] = ( - (val_on,) if isinstance(val_on, str) else tuple(val_on) - ) - data = value(*on_tuple) - if not isinstance(data, _Expr): - # Not an expression, fallback to eager series - data = None - except Exception: - data = None - - if data is None: - # Eager evaluation path - inputs = self.select(val_on) if val_on is not None else self # type: ignore - batch = inputs.to_arrow() - if val_on is not None: - # Pass individual columns as pyarrow arrays - on_list = [val_on] if isinstance(val_on, str) else list(val_on) - args = [batch.column(c) for c in on_list] - data = value(*args) - else: - # Pass the whole record batch - data = value(batch) - - if isinstance(data, pl.DataFrame) and data.width == 1: - data = data.to_series() - else: - data = value - if isinstance(data, _Expr): - processed_values.append(data) - schema_capsules.append(None) - else: - # Array-like - try: - array_capsule, schema_capsule = data.__arrow_c_array__() # type: ignore - except (AttributeError, TypeError): - if isinstance(data, pl.Series): - data = pa.array(data.to_list()) - else: - data = pa.array(data) - array_capsule, schema_capsule = data.__arrow_c_array__() - processed_values.append(array_capsule) - schema_capsules.append(schema_capsule) - - new_inner = self._container.add_columns(names, processed_values, schema_capsules) - new_container = TrackingDataContainer.__new__(TrackingDataContainer) - new_container._container = new_inner - return new_container + def add_columns(self, names: Sequence[str], values: Sequence[Any]) -> TrackingDataContainer: + res = self + for n, v in zip(names, values): + res = res.add_column(n, v) + return res def add_column_per_player( - self, name: str, value: Any, on: str | list[str] | None = None + self, name: str, value: Any, on: str | Sequence[str] | None = None ) -> TrackingDataContainer: - """ - Add a new column for every player in the container. - - Parameters - ---------- - name : str - The attribute name for the new columns. - value : array-like, Expr, or callable - The data or computation for the new columns. - on : str or list of str, optional - The player-specific attributes to use for the computation. - - Returns - ------- - TrackingDataContainer - A new container with added columns for each player. - """ - names = [] - values = [] - ons = [] - + names, values_list, ons = [], [], [] for player in self.schema().players(): col_name = DataField(entity=player, attribute=name).name() names.append(col_name) - player_on: str | list[str] | None = None if on is not None: if isinstance(on, str): player_on = DataField(entity=player, attribute=on).name() - elif isinstance(on, list): + elif isinstance(on, (list, tuple)): player_on = [DataField(entity=player, attribute=attr).name() for attr in on] - ons.append(player_on) + values_list.append(value) + res = self + for n, v, o in zip(names, values_list, ons): + res = res.add_column(n, v, on=o) + return res - if isinstance(value, _Expr) and on is not None: - # Optimized path for shared expressions: substitute placeholders - on_list = [on] if isinstance(on, str) else on - mapping = { - attr: DataField(entity=player, attribute=attr).name() for attr in on_list - } - values.append(value.replace_columns(mapping)) - continue - - if callable(value) and on is not None: - on_resolved = player_on if isinstance(player_on, list) else [player_on] # type: ignore - # Check if it returns an Expr - try: - result = value(*on_resolved) # type: ignore - if isinstance(result, _Expr): - values.append(result) - continue - except Exception: - pass + def to_arrow(self) -> pa.RecordBatch: + return pa.record_batch(self._container.to_arrow()) - values.append(value) + def to_polars(self) -> pl.DataFrame: + df = pl.from_arrow(self.to_arrow()) + if isinstance(df, pl.Series): + df = df.to_frame() + return df - return self.add_columns(names, values, on=ons) # type: ignore + def to_pandas(self) -> pd.DataFrame: + import pandas as pd - def lazy(self) -> LazyTrackingDataContainer: - """ - Converts the container to a lazy version for deferred execution. + return cast(pd.DataFrame, self.to_arrow().to_pandas()) - Returns - ------- - LazyTrackingDataContainer - The lazy version of this container. - """ - new_inner = self._container.lazy() - new_container = LazyTrackingDataContainer.__new__(LazyTrackingDataContainer) - new_container._container = new_inner - return new_container + def to_parquet(self, path: str | Path) -> None: + self._container.save_parquet(str(path)) - # --- Materialization and Visualization --- + def melt(self) -> pl.DataFrame: + import polars as pl - def materialize_frame(self, frame: int | datetime | timedelta) -> Frame: - """ - Returns the values of all fields for a specific frame. - - Parameters - ---------- - frame : int, datetime, or timedelta - The frame index or time to materialize. - - Returns - ------- - Frame - The materialized frame data. - """ - if isinstance(frame, int): - index = frame - elif isinstance(frame, datetime): - # Resolve index from timestamp using integer comparison - target_ts = int(frame.timestamp() * 1_000_000) - df = self.select(["timestamp"]).to_polars() - try: - # Use numpy for the comparison to avoid Polars Datetime eq issues - ts_array = df["timestamp"].dt.timestamp("us").to_numpy() - index = int(np.where(ts_array == target_ts)[0][0]) - except (IndexError, KeyError): - raise ValueError(f"Timestamp {frame} not found in container") - elif isinstance(frame, timedelta): - # Resolve index from offset - target_us = int(frame.total_seconds() * 1_000_000) - df = self.select(["timestamp"]).to_polars() + df = self.to_polars() + core = [ + c + for c in [ + "match/id", + "competition/id", + "season/id", + "timestamp", + "frame_id", + "period", + "period_id", + ] + if c in df.columns + ] + groups: dict[tuple, dict[str, str]] = {} + for c in df.columns: + if c in core: + continue try: - # Compare relative to first frame - ts_array = df["timestamp"].dt.timestamp("us").to_numpy() - if len(ts_array) > 0: - start_ts = ts_array[0] - index = int(np.where(ts_array == (start_ts + target_us))[0][0]) - else: - raise IndexError("Container is empty") - except (IndexError, KeyError): - # Fallback to frame_id if timestamp resolution fails - try: - df_id = self.select(["frame_id"]).to_polars() - # Assume 25fps if no metadata, or use frame_id starting at 0 - target_frame = int(frame.total_seconds() * 25) - index = int(np.where(df_id["frame_id"] == target_frame)[0][0]) - except (IndexError, KeyError): - raise ValueError(f"Timedelta {frame} could not be resolved to a frame index") - else: - raise TypeError(f"Unsupported frame type: {type(frame)}") + f = DataField(c) + if not f.entity: + continue - return self._container.materialize_frame(index) + # Check for metadata columns that shouldn't be melted + if f.attribute in ("player/id", "team/id", "entity_key"): + continue - def plot_frame( - self, - frame: int | datetime | timedelta, - ax: Any = None, - home_color: str = "red", - away_color: str = "blue", - ball_color: str = "white", - referee_color: str = "yellow", - player_size: float = 80, - ball_size: float = 50, - **kwargs: Any, - ) -> Any: - """ - Visualizes a frame using mplsoccer. - - Parameters - ---------- - frame : int, datetime, or timedelta - The frame index, timestamp, or timedelta to plot. - ax : matplotlib.axes.Axes, optional - An existing matplotlib Axes object to plot on. - home_color : str, default="red" - Color for the home team players. - away_color : str, default="blue" - Color for the away team players. - ball_color : str, default="white" - Color for the ball. - referee_color : str, default="yellow" - Color for the referees. - player_size : float, default=0.8 - Radius of the players in pitch units. - ball_size : float, default=0.5 - Radius of the ball in pitch units. - **kwargs - Additional arguments passed to the Pitch.scatter method. - - Returns - ------- - matplotlib.axes.Axes - The matplotlib Axes object. - - Raises - ------ - ImportError - If mplsoccer is not installed. - ValueError - If the coordinate system is not defined. - """ - try: - from mplsoccer import Pitch - except ImportError: - raise ImportError( - "Seems like you don't have `mplsoccer` installed. " - "Please install it using: pip install mplsoccer" - ) + key = ( + f.entity.type, + getattr(f.entity, "player_id", None), + getattr(f.entity, "team_id", None), + getattr(f.entity, "referee_id", None), + ) + if key not in groups: + groups[key] = {} + groups[key][f.attribute] = c + except Exception: + continue - if self.coordinate_system is None: - raise ValueError( - "Coordinate system must be defined to plot. " - "Use container.with_coordinate_system() to set it." + dfs = [] + for (et, pid, tid, rid), attrs in groups.items(): + t = df.select(core + list(attrs.values())).rename({v: k for k, v in attrs.items()}) + p_id = ( + "ball" + if et == "ball" + else ( + pid if et == "player" else (f"team:{tid}" if et == "team" else f"referee:{rid}") + ) + ) + t = t.with_columns( + [ + pl.lit(str(p_id)).cast(pl.String).alias("player/id"), + pl.lit(str(tid) if tid else None).cast(pl.String).alias("team/id"), + (pl.lit(str(p_id)) + pl.lit("|") + pl.lit(str(tid) if tid else "none")) + .cast(pl.String) + .alias("entity_key"), + ] ) + t = t.filter(~pl.all_horizontal(pl.col(list(attrs.keys())).is_null())) + dfs.append(t) + return pl.concat(dfs, how="diagonal") if dfs else pl.DataFrame() - dot_styles = { - "home": { - "facecolor": home_color, - "edgecolor": "white", - "linewidth": 0.8, - "s": player_size, - "zorder": 5, - }, - "away": { - "facecolor": away_color, - "edgecolor": "white", - "linewidth": 0.8, - "s": player_size, - "zorder": 5, - }, - "ball": { - "facecolor": ball_color, - "edgecolor": "black", - "s": ball_size, - "zorder": 6, - }, - "referee": { - "facecolor": referee_color, - "edgecolor": "black", - "linewidth": 0.8, - "s": player_size, - "zorder": 5, - }, - } - - dims = self.coordinate_system.to_mplsoccer() - pitch = Pitch(pitch_type=dims) - - if ax is None: - fig, ax = pitch.draw() - else: - pitch.draw(ax=ax) + def __arrow_c_schema__(self) -> Any: + return self._container.__arrow_c_schema__() - f = self.materialize_frame(frame) - schema = self.schema() + def __arrow_c_array__(self, requested_schema: Any = None) -> Any: + return self._container.__arrow_c_array__(requested_schema) - # Plot players - for tracked_team in schema.teams(): - from .schema import Team + @classmethod + def from_arrow( + cls, + data: pa.Table | pa.RecordBatch | pa.StructArray | pa.ChunkedArray, + catalog: TrackingDataCatalog | None = None, + ) -> TrackingDataContainer: + import pyarrow as pa - if not isinstance(tracked_team, Team): - continue - team_id = tracked_team.team_id - if team_id is None: - continue - style_key = "home" if "home" in team_id.lower() else "away" - style = dot_styles[style_key].copy() - style.update(kwargs) - - players = schema.players(team=team_id) - x = [] - y = [] - for p in players: - px = f[DataField(entity=p, attribute="x")] # type: ignore - py = f[DataField(entity=p, attribute="y")] # type: ignore - if px is not None and py is not None: - x.append(px) - y.append(py) - - if x: - pitch.scatter(x, y, ax=ax, label=f"Team {team_id}", **style) - - # Plot ball - bx = f[DataField(entity=Ball(), attribute="x")] # type: ignore - by = f[DataField(entity=Ball(), attribute="y")] # type: ignore - if bx is not None and by is not None: - style = dot_styles["ball"].copy() - style.update(kwargs) - pitch.scatter(bx, by, ax=ax, label="Ball", **style) - - # Plot referees - rx = [] - ry = [] - for ref in schema.referees(): - px = f[DataField(entity=ref, attribute="x")] # type: ignore - py = f[DataField(entity=ref, attribute="y")] # type: ignore - if px is not None and py is not None: - rx.append(px) - ry.append(py) - - if rx: - style = dot_styles["referee"].copy() - style.update(kwargs) - pitch.scatter(rx, ry, ax=ax, label="Referees", **style) - - return ax - - # --- Interoperability and Export --- + if isinstance(data, pa.Table): + data = data.combine_chunks() + batch = data.to_batches()[0] + data = pa.StructArray.from_arrays(batch.columns, names=batch.schema.names) + elif isinstance(data, pa.RecordBatch): + data = pa.StructArray.from_arrays(data.columns, names=data.schema.names) + elif hasattr(data, "combine_chunks"): + data = data.combine_chunks() + + assert isinstance(data, pa.StructArray) + cap_arr, cap_sch = data.__arrow_c_array__() + new_inner = _TrackingDataContainer.from_arrow(cap_arr, cap_sch, None, None) + return TrackingDataContainer(new_inner, catalog=catalog, validate=False) + + @classmethod + def pivot( + cls, df: pl.DataFrame | pa.Table | pa.StructArray | pa.RecordBatch + ) -> TrackingDataContainer: + """Pivots long-format tracking data into wide-format using Rust implementation.""" + if hasattr(df, "to_arrow"): + df = df.to_arrow() - def to_arrow(self) -> pa.RecordBatch: - """Converts the container to a pyarrow `RecordBatch`.""" import pyarrow as pa - return pa.record_batch(self._container.to_arrow()) + if isinstance(df, pa.Table): + df = df.combine_chunks() + # RecordBatch.from_arrays expects Arrays, not ChunkedArrays + df = pa.RecordBatch.from_arrays([c.chunk(0) for c in df.columns], names=df.schema.names) + elif isinstance(df, pa.StructArray): + # Convert StructArray to RecordBatch by creating a table first + df = pa.RecordBatch.from_arrays(df.flatten(), names=[f.name for f in df.type]) - def to_pandas(self) -> pd.DataFrame: - """Converts the container to a pandas `DataFrame`.""" - return cast("pd.DataFrame", self.to_arrow().to_pandas()) + # Use the underlying Rust implementation for performance + inner = _TrackingDataContainer.pivot(df) + return TrackingDataContainer(inner, validate=False) - def to_polars(self) -> pl.DataFrame: - """Converts the container to a polars `DataFrame`.""" - import polars as pl + def lazy(self) -> LazyTrackingDataContainer: + return LazyTrackingDataContainer(self._container.lazy(), catalog=self._catalog) - return cast(pl.DataFrame, pl.DataFrame(self)) + def materialize_frame(self, frame: int | datetime | timedelta) -> Frame: + """Materializes a single frame. Fixed datetime comparison.""" + if isinstance(frame, int): + if frame < 0 or frame >= self.num_rows(): + raise IndexError("Frame out of range.") + return self._container.materialize_frame(frame) + + df = self.select(["timestamp", "frame_id"]).to_polars() + if len(df) == 0: + raise ValueError("Container is empty.") + + if isinstance(frame, datetime): + target = int(frame.timestamp() * 1_000_000) + ts_values = df["timestamp"].cast(pl.Int64).to_numpy() + matches = np.where(ts_values == target)[0] + if len(matches) == 0: + raise ValueError(f"No frame found at {frame}") + idx = int(matches[0]) + elif isinstance(frame, timedelta): + if "frame_id" in df.columns: + target_fid = df["frame_id"][0] + int(frame.total_seconds() * 25) + matches = np.where(df["frame_id"].to_numpy() == target_fid)[0] + if len(matches) == 0: + raise ValueError(f"No frame found at offset {frame}") + idx = int(matches[0]) + else: + target_ts = df["timestamp"][0] + int(frame.total_seconds() * 1_000_000) + matches = np.where(df["timestamp"].cast(pl.Int64).to_numpy() >= target_ts)[0] + if len(matches) == 0: + raise ValueError(f"No frame found at offset {frame}") + idx = int(matches[0]) + else: + raise TypeError(f"Unsupported frame type: {type(frame)}") - def to_parquet(self, path: str | Path) -> None: - """Saves the container to a Parquet file.""" - self._container.save_parquet(str(path)) + return self._container.materialize_frame(idx) @classmethod def from_parquet(cls, path: str | Path) -> TrackingDataContainer: - """Loads a container from a Parquet file.""" + """Loads from a Parquet file.""" new_inner = _TrackingDataContainer.load_parquet(str(path)) - new_container = TrackingDataContainer.__new__(TrackingDataContainer) - new_container._container = new_inner - return new_container - - # --- Internal and Special Methods --- - - def __arrow_c_schema__(self) -> object: - return self._container.__arrow_c_schema__() - - def __arrow_c_array__(self, requested_schema: object | None = None) -> object: - return self._container.__arrow_c_array__(requested_schema) + return TrackingDataContainer(new_inner, validate=False) def __repr__(self) -> str: return repr(self._container) class LazyTrackingDataContainer: - """ - A lazy version of the tracking data container for deferred execution. - - LazyTrackingDataContainer allows building complex query plans that are - optimized and executed only when explicitly requested via `collect`. - """ - - _container: _LazyTrackingDataContainer - - def __init__(self, container: _LazyTrackingDataContainer): + def __init__( + self, + container: _LazyTrackingDataContainer, + catalog: TrackingDataCatalog | None = None, + is_long_storage: bool = False, + deferred_select: Sequence[str] | None = None, + cached_schema: Fields | None = None, + deferred_add_columns: list[tuple] | None = None, + ): self._container = container - - # --- Properties and Metadata --- + self._catalog = catalog + self._is_long_storage = is_long_storage + self._deferred_select = deferred_select + self._cached_schema = cached_schema + self._deferred_add_columns = deferred_add_columns or [] def schema(self) -> Fields: - """Returns the schema of the lazy container.""" - return Fields(self._container.schema) + """Fast reconstruction for long storage. Optimization: Selection pushdown.""" + if self._cached_schema: + return self._cached_schema + if not self._is_long_storage: + self._cached_schema = Fields(self._container.schema) + return self._cached_schema - @property - def coordinate_system(self) -> CoordinateSystem | None: - """Returns the active coordinate system metadata.""" - if self._container.coordinate_system is None: - return None - return CoordinateSystem._from_inner(self._container.coordinate_system) + import polars as pl - @property - def orientation_mode(self) -> OrientationMode | None: - """Returns the active orientation normalization mode.""" - return self._container.orientation_mode + ids: list[str | _DataField | _Selection] = [ + _DataField.from_name("player/id"), + _DataField.from_name("team/id"), + ] + entities = self._container.select(ids).collect() + ent_df = pl.from_arrow(entities.to_arrow()) + if isinstance(ent_df, pl.Series): + ent_df = ent_df.to_frame() + ent_df = ent_df.unique() + + phys_names = [f.name() for f in self._container.schema] + phys_attrs = [n for n in phys_names if n not in CORE_COLUMNS] + + # Start with all non-entity core columns + wide_names = [c for c in CORE_COLUMNS if c not in ("player/id", "team/id", "entity_key")] + for row in ent_df.to_dicts(): + p, t = str(row["player/id"]), str(row.get("team/id", "none")) + prefix = TrackedEntity.from_entity_key(f"{p}|{t}").cdf_prefix() + for attr in phys_attrs: + wide_names.append(f"{prefix}{attr}") + + self._cached_schema = Fields(list(dict.fromkeys(wide_names))) + return self._cached_schema + + def select(self, items: str | Sequence[str]) -> LazyTrackingDataContainer: + """Lazily selects columns. Supports selection pushdown.""" + if self._is_long_storage: + sch = self.schema() + resolved = [] + inputs = items if isinstance(items, (list, tuple)) else [items] + for it in inputs: + if isinstance(it, str): + if "*" in it: + resolved.extend(sch.match(it)) + else: + resolved.append(it) + elif isinstance(it, DataField): + resolved.append(it.name()) + + resolved = list(dict.fromkeys(resolved)) + core_set = { + "match/id", + "competition/id", + "season/id", + "timestamp", + "frame_id", + "period", + "player/id", + "team/id", + "entity_key", + } + phys_attrs, entities = set(), set() + for name in resolved: + if name in core_set: + continue + try: + f = DataField(name) + phys_attrs.add(f.attribute) + if f.entity: + if f.entity.type != "team": + pid = getattr(f.entity, "player_id", "ball") + else: + pid = f"team:{f.entity.team_id}" + entities.add((str(pid), str(getattr(f.entity, "team_id", "none")))) + except Exception: + continue - @property - def selector(self) -> ColumnSelector: - """Returns the selector for building column expressions.""" - return ColumnSelector(self._container.selector) + new_inner = self._container + if entities: + from ._tracking_data_container import col, lit + + pred = None + for pid, tid in entities: + p_cond = col("player/id").eq(lit(pid)) + if tid != "none": + p_cond = p_cond & col("team/id").eq(lit(tid)) + pred = p_cond if pred is None else (pred | p_cond) + if pred: + new_inner = new_inner.filter(pred) + + phys_names = [f.name() for f in self._container.schema] + phys_sel = [n for n in core_set if n in phys_names] + phys_sel.extend([n for n in phys_attrs if n in phys_names]) + + rust_sel: list[str | _DataField | _Selection] = [ + _DataField.from_name(n) for n in phys_sel + ] + return LazyTrackingDataContainer( + new_inner.select(rust_sel), + catalog=self._catalog, + is_long_storage=True, + deferred_select=resolved, + cached_schema=Fields(resolved), + deferred_add_columns=self._deferred_add_columns, + ) - # --- Selections and Data Additions --- - - def select(self, items: str | list[str | DataField | Selection]) -> LazyTrackingDataContainer: - """ - Lazily select columns from the container. - - Parameters - ---------- - items : str or list of str or DataField or Selection - The columns to select. - - Returns - ------- - LazyTrackingDataContainer - A new lazy container with the selection added to the plan. - """ - processed_items: str | list[str | _DataField | _Selection] | _DataField | _Selection - if isinstance(items, list): - processed_items = [] - for i in items: - if isinstance(i, DataField): - processed_items.append(i._field) - elif isinstance(i, Selection): - processed_items.append(i._selection) + inputs = items if isinstance(items, (list, tuple)) else [items] + processed: list[str | _DataField | _Selection] = [] + for i in inputs: + if isinstance(i, str): + if "*" in i: + for m in self.schema().match(i): + processed.append(_DataField.from_name(m)) else: - processed_items.append(i) - elif isinstance(items, DataField): - processed_items = items._field - elif isinstance(items, Selection): - processed_items = items._selection - else: - processed_items = items - - new_inner = self._container.select(processed_items) # type: ignore - return LazyTrackingDataContainer(new_inner) - - def slice(self, offset: int, length: int) -> LazyTrackingDataContainer: - """ - Lazily extract a slice of frames from the container. - - Parameters - ---------- - offset : int - The starting frame index. - length : int - The number of frames to include. - - Returns - ------- - LazyTrackingDataContainer - A new lazy container with the slice added to the plan. - """ - new_inner = self._container.slice(offset, length) - return LazyTrackingDataContainer(new_inner) - - def filter(self, predicate: _Expr) -> LazyTrackingDataContainer: - """ - Lazily filter rows from the container. - - Parameters - ---------- - predicate : _Expr - The expression defining the filter condition. - - Returns - ------- - LazyTrackingDataContainer - A new lazy container with the filter added to the plan. - """ - new_inner = self._container.filter(predicate) - return LazyTrackingDataContainer(new_inner) - - def transform_coordinates(self, to_system: CoordinateSystem) -> LazyTrackingDataContainer: - """ - Lazily transforms coordinates to a new spatial system. - - Parameters - ---------- - to_system : CoordinateSystem - The target coordinate system. - - Returns - ------- - LazyTrackingDataContainer - A new lazy container with transformed coordinates added to the plan. - """ - new_inner = self._container.transform_coordinates(to_system._inner) - return LazyTrackingDataContainer(new_inner) + processed.append(_DataField.from_name(i)) + elif isinstance(i, DataField): + processed.append(i._field) + elif isinstance(i, Selection): + processed.append(i._inner) + elif isinstance(i, (_DataField, _Selection)): + processed.append(i) + + new_cache = None + if self._cached_schema: + names = [] + for itm in inputs: + if isinstance(itm, str): + names.extend(self._cached_schema.match(itm)) + else: + names.append(str(itm)) + new_cache = Fields(list(dict.fromkeys(names))) - def transform_orientation(self, mode: str | OrientationMode) -> LazyTrackingDataContainer: - """ - Lazily normalizes coordinates based on playing direction. - - Parameters - ---------- - mode : str or OrientationMode - The orientation normalization mode. - - Returns - ------- - LazyTrackingDataContainer - A new lazy container with normalized coordinates added to the plan. - """ - if not isinstance(mode, str): - mode = str(mode) - new_inner = self._container.transform_orientation(mode) - return LazyTrackingDataContainer(new_inner) + return LazyTrackingDataContainer( + self._container.select(processed), + catalog=self._catalog, + cached_schema=new_cache, + deferred_add_columns=self._deferred_add_columns, + ) def add_column( - self, name: str, value: Any, on: str | list[str] | None = None + self, name: str, value: Any, on: str | Sequence[str] | None = None ) -> LazyTrackingDataContainer: - """ - Lazily add a new column to the container. - - Parameters - ---------- - name : str - The name of the new column. - value : Expr, array-like, or callable - The computation or data for the new column. - on : str or list of str, optional - The columns required for the computation (required for callables). - - Returns - ------- - LazyTrackingDataContainer - A new lazy container with the column addition added to the plan. - """ - import pyarrow as pa - - from ._tracking_data_container import Expr as _PyExpr - - if hasattr(value, "_expr") and isinstance(value._expr, _PyExpr): - new_inner = self._container.add_column(name, value._expr, None) - elif isinstance(value, _PyExpr): - new_inner = self._container.add_column(name, value, None) - elif callable(value): - if on is None: - raise ValueError( - "The 'on' parameter must be provided when using a callable in lazy mode" - ) - # Ensure on is a list of strings - on_list = [on] if isinstance(on, str) else list(on) - new_inner = self._container.add_column_udf(name, value, on_list) - else: - data = pa.array(value) - array_capsule, schema_capsule = data.__arrow_c_array__() - new_inner = self._container.add_column(name, array_capsule, schema_capsule) - - return LazyTrackingDataContainer(new_inner) - - def add_columns( - self, - names: list[str], - values: list[Any], - on: list[str | list[str] | None] | None = None, - ) -> LazyTrackingDataContainer: - """ - Lazily add multiple new columns to the container. - - Parameters - ---------- - names : list of str - The names for the new columns. - values : list of Expr, array-like, or callable - The data or computations for the new columns. - on : list of str or list of list of str, optional - The columns to pass to each callable value. - - Returns - ------- - LazyTrackingDataContainer - A new lazy container including the new columns. - """ - import pyarrow as pa - - from ._tracking_data_container import Expr as _PyExpr - - processed_values = [] - schema_capsules: list[Any] = [] - - for i, (name, value) in enumerate(zip(names, values)): - val_on = on[i] if on is not None else None - if callable(value): - # Eager evaluation for callables in lazy mode is not supported - # unless they return an Expr. - if val_on is None: - raise ValueError( - "The 'on' parameter must be provided when using a callable in lazy mode" - ) - # Try to evaluate as expression - on_tuple: tuple[str, ...] = (val_on,) if isinstance(val_on, str) else tuple(val_on) - data = value(*on_tuple) - if not isinstance(data, _PyExpr): - raise TypeError("Callables in lazy mode must return an Expr") - elif hasattr(value, "_expr") and isinstance(value._expr, _PyExpr): - data = value._expr - else: - data = value - - if isinstance(data, _PyExpr): - processed_values.append(data) - schema_capsules.append(None) - else: - # Array-like - array_data = pa.array(data) - array_capsule, schema_capsule = array_data.__arrow_c_array__() - processed_values.append(array_capsule) - schema_capsules.append(schema_capsule) - - new_inner = self._container.add_columns(names, processed_values, schema_capsules) - return LazyTrackingDataContainer(new_inner) - - def add_column_per_player( - self, name: str, value: Any, on: str | list[str] | None = None - ) -> LazyTrackingDataContainer: - """ - Lazily add a new column for every player in the container. - - Parameters - ---------- - name : str - The attribute name for the new columns. - value : Expr, array-like, or callable - The computation or data for the new columns. - on : str or list of str, optional - The player-specific attributes to use for the computation. - - Returns - ------- - LazyTrackingDataContainer - A new lazy container with the per-player columns added to the plan. - """ - names = [] - values = [] - ons = [] - - from ._tracking_data_container import Expr as _PyExpr - - for player in self.schema().players(): - col_name = DataField(entity=player, attribute=name).name() - names.append(col_name) - - player_on: str | list[str] | None = None - if on is not None: - if isinstance(on, str): - player_on = DataField(entity=player, attribute=on).name() - elif isinstance(on, list): - player_on = [DataField(entity=player, attribute=attr).name() for attr in on] - - ons.append(player_on) - - if ( - isinstance(value, _PyExpr) - or (hasattr(value, "_expr") and isinstance(value._expr, _PyExpr)) - ) and on is not None: - expr = value._expr if hasattr(value, "_expr") else value - on_list = [on] if isinstance(on, str) else on - mapping = { - attr: DataField(entity=player, attribute=attr).name() for attr in on_list - } - values.append(expr.replace_columns(mapping)) - continue - - if callable(value) and on is not None: - on_resolved = player_on if isinstance(player_on, list) else [player_on] # type: ignore - # Check if it returns an Expr - try: - result = value(*on_resolved) # type: ignore - if isinstance(result, _PyExpr) or ( - hasattr(result, "_expr") and isinstance(result._expr, _PyExpr) - ): - expr = result._expr if hasattr(result, "_expr") else result - values.append(expr) - continue - except Exception: - pass - - values.append(value) + if isinstance(value, _Expr): + return LazyTrackingDataContainer( + self._container.add_column(name, value), + catalog=self._catalog, + is_long_storage=self._is_long_storage, + deferred_select=self._deferred_select, + cached_schema=self._cached_schema, + deferred_add_columns=self._deferred_add_columns, + ) - return self.add_columns(names, values, on=ons) + if callable(value): + new_deferred = list(self._deferred_add_columns) + new_deferred.append((name, value, on)) + return LazyTrackingDataContainer( + self._container, + catalog=self._catalog, + is_long_storage=self._is_long_storage, + deferred_select=self._deferred_select, + cached_schema=self._cached_schema, + deferred_add_columns=new_deferred, + ) - # --- Execution --- + raise TrackingDataContainerError( + "Adding non-expression columns to lazy containers is not yet supported." + ) def collect(self) -> TrackingDataContainer: - """ - Executes the lazy plan and returns an eager `TrackingDataContainer`. - - Returns - ------- - TrackingDataContainer - The resulting eager container. - """ - new_inner = self._container.collect() - return TrackingDataContainer(new_inner) + """Executes the plan and pivots if necessary.""" + try: + inner = self._container.collect() + res = TrackingDataContainer(inner, catalog=self._catalog, validate=False) + + for name, value, on in self._deferred_add_columns: + res = res.add_column(name, value, on=on) + + if self._is_long_storage: + res = TrackingDataContainer.pivot(res.to_arrow()) + res._catalog = self._catalog + if self._deferred_select: + logger.debug(f"Wide container columns before selection: {res.schema().names}") + res = res.select(list(self._deferred_select)) + logger.debug(f"Wide container columns after selection: {res.schema().names}") + return res + except Exception as e: + raise TrackingDataContainerError(f"Collection failed: {e}") - # --- Internal and Special Methods --- + def slice(self, offset: int, length: int) -> LazyTrackingDataContainer: + return LazyTrackingDataContainer( + self._container.slice(offset, length), + catalog=self._catalog, + is_long_storage=self._is_long_storage, + cached_schema=self._cached_schema, + deferred_add_columns=self._deferred_add_columns, + ) - def __arrow_c_schema__(self) -> object: - return self._container.__arrow_c_schema__() + def filter(self, predicate: _Expr) -> LazyTrackingDataContainer: + return LazyTrackingDataContainer( + self._container.filter(predicate), + catalog=self._catalog, + is_long_storage=self._is_long_storage, + cached_schema=self._cached_schema, + deferred_add_columns=self._deferred_add_columns, + ) + + def transform_coordinates(self, sys: CoordinateSystem) -> LazyTrackingDataContainer: + return LazyTrackingDataContainer( + self._container.transform_coordinates(sys._inner), + catalog=self._catalog, + is_long_storage=self._is_long_storage, + cached_schema=self._cached_schema, + deferred_add_columns=self._deferred_add_columns, + ) - def __arrow_c_stream__(self, requested_schema: object | None = None) -> object: - return self._container.__arrow_c_stream__(requested_schema) + def transform_orientation(self, mode: str | OrientationMode) -> LazyTrackingDataContainer: + return LazyTrackingDataContainer( + self._container.transform_orientation(str(mode)), + catalog=self._catalog, + is_long_storage=self._is_long_storage, + cached_schema=self._cached_schema, + deferred_add_columns=self._deferred_add_columns, + ) + + def __arrow_c_stream__(self, rs: Any = None) -> Any: + return self._container.__arrow_c_stream__(rs) def __repr__(self) -> str: return repr(self._container) diff --git a/python/tracking_data_container/schema.py b/python/tracking_data_container/schema.py index 9d05399..307a2fb 100644 --- a/python/tracking_data_container/schema.py +++ b/python/tracking_data_container/schema.py @@ -44,6 +44,78 @@ def type(self) -> str: return "referee" return "unknown" + @property + def team_id(self) -> str | None: + return self._entity.team_id() + + @property + def player_id(self) -> str | None: + return self._entity.player_id() + + @property + def referee_id(self) -> str | None: + return self._entity.player_id() if self.is_referee else None + + def entity_key(self) -> str: + """Returns a stable unique identifier for this entity (p_id|t_id).""" + p_id = "ball" + t_id = "none" + if self.is_player: + p_id = self.player_id or "unknown" + t_id = self.team_id or "none" + elif self.is_team: + t_id = self.team_id or "unknown" + p_id = f"team:{t_id}" + elif self.is_referee: + p_id = f"referee:{self.referee_id or 'unknown'}" + t_id = "none" + + # Normalize "none" strings + if t_id is None or str(t_id).lower() == "none" or str(t_id).lower() == "nan": + t_id = "none" + return f"{p_id}|{t_id}" + + def cdf_prefix(self) -> str: + """Returns the CDF prefix for this entity (e.g., 'teams/T1/players/P1/').""" + if self.is_ball: + return "ball/" + if self.is_team: + return f"teams/{self.team_id}/" + if self.is_referee: + return f"referees/{self.referee_id}/" + if self.is_player: + t_id = self.team_id + if t_id and str(t_id).lower() not in ("none", "nan"): + return f"teams/{t_id}/players/{self.player_id}/" + return f"other/{self.player_id}/" + return "" + + @staticmethod + def from_entity_key(key: str) -> TrackedEntity: + """Reconstructs a TrackedEntity from its entity key.""" + try: + parts = key.split("|") + p_id = parts[0] + t_id = parts[1] if len(parts) > 1 else "none" + except (ValueError, IndexError): + p_id, t_id = key, "none" + + if p_id == "ball": + return Ball() + if p_id.startswith("team:"): + # Use provided t_id if available, otherwise extract from p_id + team_id = t_id if t_id != "none" else p_id[5:] + return Team(team_id) + if p_id.startswith("referee:"): + return Referee(p_id[8:]) + + if t_id != "none": + return Player(t_id, p_id) + + # We don't have a direct "OtherPlayer" class, so we use a dummy name to + # construct a DataField and get its entity + return TrackedEntity._from_inner(_DataField.from_name(f"other/{p_id}/x").entity()) + @staticmethod def _from_inner(entity: _TrackedEntity) -> TrackedEntity: if entity.is_ball(): @@ -70,14 +142,6 @@ def _from_inner(entity: _TrackedEntity) -> Player: instance._entity = entity return instance - @property - def team_id(self) -> str: - return self._entity.team_id() or "" - - @property - def player_id(self) -> str: - return self._entity.player_id() or "" - def __repr__(self) -> str: return f"Player(team={self.team_id}, id={self.player_id})" @@ -114,10 +178,6 @@ def _from_inner(entity: _TrackedEntity) -> Team: instance._entity = entity return instance - @property - def team_id(self) -> str: - return self._entity.team_id() or "" - def __repr__(self) -> str: return f"Team(id={self.team_id})" @@ -137,10 +197,6 @@ def _from_inner(entity: _TrackedEntity) -> Referee: instance._entity = entity return instance - @property - def referee_id(self) -> str: - return self._entity.player_id() or "" - def __repr__(self) -> str: return f"Referee(id={self.referee_id})" @@ -165,16 +221,7 @@ def __new__( if name is not None: inner = _DataField.from_name(name) elif entity is not None and attribute is not None: - if isinstance(entity, Ball): - path = f"ball/{attribute}" - elif isinstance(entity, Player): - path = f"teams/{entity.team_id}/players/{entity.player_id}/{attribute}" - elif isinstance(entity, Team): - path = f"teams/{entity.team_id}/{attribute}" - elif isinstance(entity, Referee): - path = f"referees/{entity.referee_id}/{attribute}" - else: - path = attribute + path = f"{entity.cdf_prefix()}{attribute}" inner = _DataField.from_name(path) elif attribute is not None: inner = _DataField.from_name(attribute) @@ -216,6 +263,14 @@ def team_id(self) -> str | None: def player_id(self) -> str | None: return getattr(self.entity, "player_id", None) + def entity_key(self) -> str | None: + """Returns the stable entity key for this field, if applicable.""" + return self.entity.entity_key() if self.entity else None + + def cdf_prefix(self) -> str | None: + """Returns the CDF prefix for this field, if applicable.""" + return self.entity.cdf_prefix() if self.entity else None + def __hash__(self) -> int: return hash(self.name()) @@ -270,6 +325,42 @@ def names(self) -> list[str]: """Returns a list of all column names in the schema.""" return [f.name() for f in self] + def match(self, pattern: str) -> list[str]: + """Returns a list of column names that match the given wildcard pattern.""" + import fnmatch + import re + + all_names = self.names() + if pattern in all_names: + return [pattern] + + # Support recursive wildcard ** + if "**" in pattern: + # Escape regex special characters manually + # but preserve glob characters * and / + reg = pattern + for char in ".+?()[]{}^$|": + reg = reg.replace(char, f"\\{char}") + + # Use placeholders to avoid conflicting replacements + # 1. Handle **/ specifically (optional intermediate levels) + reg = reg.replace("**/", "___REC_SLASH___") + # 2. Handle ** remaining + reg = reg.replace("**", "___REC___") + # 3. Handle standard * + reg = reg.replace("*", "___STD___") + + # Map placeholders to regex + reg = reg.replace("___REC_SLASH___", "(?:.*/)?") + reg = reg.replace("___REC___", ".*") + reg = reg.replace("___STD___", "[^/]*") + + regex = re.compile(f"^{reg}$") + return [n for n in all_names if regex.match(n)] + + # Standard fnmatch for simple patterns + return fnmatch.filter(all_names, pattern) + def validate(self) -> None: import warnings diff --git a/uv.lock b/uv.lock index e3ffb9b..132045c 100644 --- a/uv.lock +++ b/uv.lock @@ -177,6 +177,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, ] +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + [[package]] name = "anyio" version = "4.12.1" @@ -227,6 +236,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[package]] +name = "cachetools" +version = "6.2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/91/d9ae9a66b01102a18cd16db0cf4cd54187ffe10f0865cc80071a4104fbb3/cachetools-6.2.6.tar.gz", hash = "sha256:16c33e1f276b9a9c0b49ab5782d901e3ad3de0dd6da9bf9bcd29ac5672f2f9e6", size = 32363, upload-time = "2026-01-27T20:32:59.956Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/45/f458fa2c388e79dd9d8b9b0c99f1d31b568f27388f2fdba7bb66bbc0c6ed/cachetools-6.2.6-py3-none-any.whl", hash = "sha256:8c9717235b3c651603fff0076db52d6acbfd1b338b8ed50256092f7ce9c85bda", size = 11668, upload-time = "2026-01-27T20:32:58.527Z" }, +] + [[package]] name = "certifi" version = "2026.2.25" @@ -1182,6 +1200,128 @@ http = [ { name = "aiohttp", marker = "python_full_version >= '3.10'" }, ] +[[package]] +name = "greenlet" +version = "3.2.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/f5/3e9eafb4030588337b2a2ae4df46212956854e9069c07b53aa3caabafd47/greenlet-3.2.5.tar.gz", hash = "sha256:c816554eb33e7ecf9ba4defcb1fd8c994e59be6b4110da15480b3e7447ea4286", size = 191501, upload-time = "2026-02-20T20:08:51.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/d6/b3db928fc329b1b19ba32ffe143d2305f3aaafc583f5e1074c74ec445189/greenlet-3.2.5-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:34cc7cf8ab6f4b85298b01e13e881265ee7b3c1daf6bc10a2944abc15d4f87c3", size = 275803, upload-time = "2026-02-20T20:06:42.541Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ff/ab0ad4ff3d9e1faa266de4f6c79763b33fccd9265995f2940192494cc0ec/greenlet-3.2.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c11fe0cfb0ce33132f0b5d27eeadd1954976a82e5e9b60909ec2c4b884a55382", size = 633556, upload-time = "2026-02-20T20:30:41.594Z" }, + { url = "https://files.pythonhosted.org/packages/da/dd/7b3ac77099a1671af8077ecedb12c9a1be1310e4c35bb69fd34c18ab6093/greenlet-3.2.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a145f4b1c4ed7a2c94561b7f18b4beec3d3fb6f0580db22f7ed1d544e0620b34", size = 644943, upload-time = "2026-02-20T20:37:23.084Z" }, + { url = "https://files.pythonhosted.org/packages/56/f0/bea7e7909ea9045b0c5055dad1ec9b81c82b761b4567e625f4f8349acfa1/greenlet-3.2.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:edbf4ab9a7057ee430a678fe2ef37ea5d69125d6bdc7feb42ed8d871c737e63b", size = 640849, upload-time = "2026-02-20T20:43:57.305Z" }, + { url = "https://files.pythonhosted.org/packages/0f/36/84630e9ff1dfc8b7690957c0f77834a84eabdbd9c4977c3a2d0cbd5325c2/greenlet-3.2.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc1d01bdd67db3e5711e6246e451d7a0f75fae7bbf40adde129296a7f9aa7cc9", size = 639841, upload-time = "2026-02-20T20:07:17.473Z" }, + { url = "https://files.pythonhosted.org/packages/12/c4/6a2ee6c676dea7a05a3c3c1291fbc8ea44f26456b0accc891471293825af/greenlet-3.2.5-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd593db7ee1fa8a513a48a404f8cc4126998a48025e3f5cbbc68d51be0a6bf66", size = 588813, upload-time = "2026-02-20T20:07:56.171Z" }, + { url = "https://files.pythonhosted.org/packages/01/c0/75e75c2c993aa850292561ec80f5c263e3924e5843aa95a38716df69304c/greenlet-3.2.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ac8db07bced2c39b987bba13a3195f8157b0cfbce54488f86919321444a1cc3c", size = 1117377, upload-time = "2026-02-20T20:32:48.452Z" }, + { url = "https://files.pythonhosted.org/packages/ee/03/e38ebf9024a0873fe8f60f5b7bc36bfb3be5e13efe4d798240f2d1f0fb73/greenlet-3.2.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4544ab2cfd5912e42458b13516429e029f87d8bbcdc8d5506db772941ae12493", size = 1141246, upload-time = "2026-02-20T20:06:23.576Z" }, + { url = "https://files.pythonhosted.org/packages/d8/7b/c6e1192c795c0c12871e199237909a6bd35757d92c8472c7c019959b8637/greenlet-3.2.5-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:acabf468466d18017e2ae5fbf1a5a88b86b48983e550e1ae1437b69a83d9f4ac", size = 276916, upload-time = "2026-02-20T20:06:18.166Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b6/9887b559f3e1952d23052ec352e9977e808a2246c7cb8282a38337221e88/greenlet-3.2.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:472841de62d60f2cafd60edd4fd4dd7253eb70e6eaf14b8990dcaf177f4af957", size = 636107, upload-time = "2026-02-20T20:30:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/8a/be/e3e48b63bbc27d660fa1d98aecb64906b90a12e686a436169c1330ef34b2/greenlet-3.2.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d951e7d628a6e8b68af469f0fe4f100ef64c4054abeb9cdafbfaa30a920c950", size = 648240, upload-time = "2026-02-20T20:37:24.608Z" }, + { url = "https://files.pythonhosted.org/packages/17/f6/2cbe999683f759f14f598234f04ae8ba6f22953a624b3a7a630003e6bfff/greenlet-3.2.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:87b791dd0e031a574249af717ac36f7031b18c35329561c1e0368201c18caf1f", size = 644170, upload-time = "2026-02-20T20:43:59.002Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ac/e731ed62576e91e533b36d0d97325adc2786674ab9e48ed8a6a24f4ef4e9/greenlet-3.2.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8317d732e2ae0935d9ed2af2ea876fa714cf6f3b887a31ca150b54329b0a6e9", size = 643313, upload-time = "2026-02-20T20:07:19.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/64/99e5cdceb494bd4c1341c45b93f322601d2c8a5e1e4d1c7a2d24c5ed0570/greenlet-3.2.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce8aed6fdd5e07d3cbb988cbdc188266a4eb9e1a52db9ef5c6526e59962d3933", size = 591295, upload-time = "2026-02-20T20:07:57.286Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e9/968e11f388c2b8792d3b8b40a57984c894a3b4745dae3662dce722653bc5/greenlet-3.2.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:60c06b502d56d5451f60ca665691da29f79ed95e247bcf8ce5024d7bbe64acb9", size = 1120277, upload-time = "2026-02-20T20:32:50.103Z" }, + { url = "https://files.pythonhosted.org/packages/cb/2c/b5f2c4c68d753dce08218dc5a6b21d82238fdfdc44309032f6fe24d285e6/greenlet-3.2.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d2a78e6f1bf3f1672df91e212a2f8314e1e7c922f065d14cbad4bc815059467", size = 1145746, upload-time = "2026-02-20T20:06:26.296Z" }, + { url = "https://files.pythonhosted.org/packages/ad/32/022b21523eee713e7550162d5ca6aed23f913cc2c6232b154b9fd9badc07/greenlet-3.2.5-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:2acb30e77042f747ca81f0a10cc153296567e92e666c5e1b117f4595afd43352", size = 278412, upload-time = "2026-02-20T20:03:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/90/c5/8a3b0ed3cc34d8b988a44349437dfa0941f9c23ac108175f7b4ccea97111/greenlet-3.2.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:393c03c26c865f17f31d8db2f09603fadbe0581ad85a5d5908b131549fc38217", size = 644616, upload-time = "2026-02-20T20:30:44.823Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2c/2627bea183554695016af6cae93d7474fa90f61e5a6601a84ae7841cb720/greenlet-3.2.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:04e6a202cde56043fd355fefd1552c4caa5c087528121871d950eb4f1b51fa99", size = 658813, upload-time = "2026-02-20T20:37:26.255Z" }, + { url = "https://files.pythonhosted.org/packages/44/c6/a80fc96f7cca7962dd972875d12c52dfabc94cb02bfeb19f3e7e169fca44/greenlet-3.2.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d5583b2ffa677578a384337ee13125bdf9a427485d689014b39d638a4f3d8dbe", size = 653512, upload-time = "2026-02-20T20:44:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/2f/1b/75a5aeff487a26ba427a3837da6372f1fe6f2a9c6b2898e28ac99d491c11/greenlet-3.2.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:45fcea7b697b91290b36eafc12fff479aca6ba6500d98ef6f34d5634c7119cbe", size = 655426, upload-time = "2026-02-20T20:07:20.124Z" }, + { url = "https://files.pythonhosted.org/packages/53/91/9b5dfb4f3c88f8247c7a8f4c3759f0740bfa6bb0c59a9f6bf938e913df56/greenlet-3.2.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f96e2bb8a56b7e1aed1dbfbbe0050cb2ecca99c7c91892fd1771e3afab63b3e3", size = 611138, upload-time = "2026-02-20T20:07:58.966Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8d/d0b086410512d9859c84e9242a9b341de9f5566011ddf3a3f6886b842b61/greenlet-3.2.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d7456e67b0be653dfe643bb37d9566cd30939c80f858e2ce6d2d54951f75b14a", size = 1126896, upload-time = "2026-02-20T20:32:52.198Z" }, + { url = "https://files.pythonhosted.org/packages/ef/37/59fe12fe456e84ced6ba71781e28cde52a3124d1dd2077bc1727021f49fd/greenlet-3.2.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5ceb29d1f74c7280befbbfa27b9bf91ba4a07a1a00b2179a5d953fc219b16c42", size = 1154779, upload-time = "2026-02-20T20:06:27.583Z" }, + { url = "https://files.pythonhosted.org/packages/dd/95/d5d332fb73affaf7a1fbe80e49c2c7eae4f17c645af24a3b3fa25736d6f0/greenlet-3.2.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:f2cc88b50b9006b324c1b9f5f3552f9d4564c78af57cdfb4c7baf4f0aa089146", size = 277166, upload-time = "2026-02-20T20:03:57.077Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/89458e20db5a4f1c64f9a0191561227e76d809941ca2d7529006d17d3450/greenlet-3.2.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e66872daffa360b2537170b73ad530f14fa31785b1bc78080125d92edf0a6def", size = 644674, upload-time = "2026-02-20T20:30:46.118Z" }, + { url = "https://files.pythonhosted.org/packages/90/f8/9962175d2f2eaa629a7fd7545abacc8c4deda3baa4e52c1526d2eb5f5546/greenlet-3.2.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c5445ddb7b586d870dad32ca9fc47c287d6022a528d194efdb8912093c5303ad", size = 658834, upload-time = "2026-02-20T20:37:27.466Z" }, + { url = "https://files.pythonhosted.org/packages/81/71/52c21a7106ce5218aa6fa59ec32825b2655f875a09b69f68bd3e5d01feb3/greenlet-3.2.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd904626b8779810062cb455514594776e3cba3b8c0ba4939894df9f7b384971", size = 653091, upload-time = "2026-02-20T20:44:01.927Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d7/826d0e080f0a7ad5ec47c8d143bbd3ca0887657bb806595fe2434d12938a/greenlet-3.2.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:752c896a8c976548faafe8a306d446c6a4c68d4fd24699b84d4393bd9ac69a8e", size = 655760, upload-time = "2026-02-20T20:07:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/41/cc/33bd4c2f816be8c8e16f71740c4130adf3a66a3dd2ba29de72b9d8dd1096/greenlet-3.2.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499b809e7738c8af0ff9ac9d5dd821cb93f4293065a9237543217f0b252f950a", size = 614132, upload-time = "2026-02-20T20:08:00.351Z" }, + { url = "https://files.pythonhosted.org/packages/48/79/f3891dcfc59097474a53cc3c624f2f2465e431ab493bda043b8c873fb20a/greenlet-3.2.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2c7429f6e9cea7cbf2637d86d3db12806ba970f7f972fcab39d6b54b4457cbaf", size = 1125286, upload-time = "2026-02-20T20:32:54.032Z" }, + { url = "https://files.pythonhosted.org/packages/ca/47/212b47e6d2d7a04c4083db1af2fdd291bc8fe99b7e3571bfa560b65fc361/greenlet-3.2.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a5e4b25e855800fba17713020c5c33e0a4b7a1829027719344f0c7c8870092a2", size = 1152825, upload-time = "2026-02-20T20:06:29Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9d/4e9b941be05f8da7ba804c6413761d2c11cca05994cbf0a015bd729419f0/greenlet-3.2.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:7123b29e6bad2f3f89681be4ef316480fca798ebe8d22fbaced9cc3775007a4f", size = 277627, upload-time = "2026-02-20T20:06:04.798Z" }, + { url = "https://files.pythonhosted.org/packages/23/cb/a73625c9a35138330014ecf3740c0d62e0c2b5e7279bb7f2586b1b199fac/greenlet-3.2.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6e8fe0c72603201a86b2e038daf9b6c8570715f8779566419cff543b6ace88de", size = 690001, upload-time = "2026-02-20T20:30:47.754Z" }, + { url = "https://files.pythonhosted.org/packages/83/49/6d1531109507bce7dfb23acf57a87013627ed3ac058851176e443a6a9134/greenlet-3.2.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:050703a60603db0e817364d69e048c70af299040c13a7e67792b9e62d4571196", size = 702953, upload-time = "2026-02-20T20:37:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/90/ac/6d8fff3b273fc60ad4b46f8411fe91c1e4cca064dfff68d096bc982fa6d0/greenlet-3.2.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:04633da773ae432649a3f092a8e4add390732cc9e1ab52c8ff2c91b8dc86f202", size = 698353, upload-time = "2026-02-20T20:44:03.547Z" }, + { url = "https://files.pythonhosted.org/packages/f7/38/f958ee90fab93529b30cc1e4a59b27c1112b640570043a84af84da3b3b98/greenlet-3.2.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6712bfd520530eb67331813f7112d3ee18e206f48b3d026d8a96cd2d2ad20251", size = 698995, upload-time = "2026-02-20T20:07:22.663Z" }, + { url = "https://files.pythonhosted.org/packages/51/c1/a603906e79716d61f08afedaf8aed62017661457aef233d62d6e57ecd511/greenlet-3.2.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bc06a78fa3ffbe2a75f1ebc7e040eacf6fa1050a9432953ab111fbbbf0d03c1", size = 661175, upload-time = "2026-02-20T20:08:01.477Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8f/f880ff4587d236b4d06893fb34da6b299aa0d00f6c8259673f80e1b6d63c/greenlet-3.2.5-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:dbe0e81e24982bb45907ca20152b31c2e3300ca352fdc4acbd4956e4a2cbc195", size = 274946, upload-time = "2026-02-20T20:05:21.979Z" }, + { url = "https://files.pythonhosted.org/packages/3c/50/f6c78b8420187fdfe97fcf2e6d1dd243a7742d272c32fd4d4b1095474b37/greenlet-3.2.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:15871afc0d78ec87d15d8412b337f287fc69f8f669346e391585824970931c48", size = 631781, upload-time = "2026-02-20T20:30:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/26/d6/3277f92e1961e6e9f41d9f173ea74b5c1f7065072637669f761626f26cc0/greenlet-3.2.5-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5bf0d7d62e356ef2e87e55e46a4e930ac165f9372760fb983b5631bb479e9d3a", size = 643740, upload-time = "2026-02-20T20:37:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8a/c37b87659378759f158dbe03eaeb7ed002a8968f1c649b2972f5323f99b2/greenlet-3.2.5-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:e3f03ddd7142c758ab41c18089a1407b9959bd276b4e6dfbd8fd06403832c87a", size = 639098, upload-time = "2026-02-20T20:44:07.287Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6a/4f79d2e7b5ef3723fc5ffea0d6cb22627e5f95e0f19c973fa12bf1cf7891/greenlet-3.2.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6dff6433742073e5b6ad40953a78a0e8cddcb3f6869e5ea635d29a810ca5e7d0", size = 638382, upload-time = "2026-02-20T20:07:23.883Z" }, + { url = "https://files.pythonhosted.org/packages/4d/59/7aadf33f23c65dbf4db27e7f5b60c414797a61e954352ae4a86c5c8b0553/greenlet-3.2.5-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdd67619cefe1cc9fcab57c8853d2bb36eca9f166c0058cc0d428d471f7c785c", size = 587516, upload-time = "2026-02-20T20:08:02.841Z" }, + { url = "https://files.pythonhosted.org/packages/1d/46/b3422959f830de28a4eea447414e6bd7b980d755892f66ab52ad805da1c4/greenlet-3.2.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3828b309dfb1f117fe54867512a8265d8d4f00f8de6908eef9b885f4d8789062", size = 1115818, upload-time = "2026-02-20T20:32:55.786Z" }, + { url = "https://files.pythonhosted.org/packages/54/4a/3d1c9728f093415637cf3696909fa10852632e33e68238fb8ca60eb90de1/greenlet-3.2.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:67725ae9fea62c95cf1aa230f1b8d4dc38f7cd14f6103d1df8a5a95657eb8e54", size = 1140219, upload-time = "2026-02-20T20:06:30.334Z" }, +] + +[[package]] +name = "greenlet" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/3f/9859f655d11901e7b2996c6e3d33e0caa9a1d4572c3bc61ed0faa64b2f4c/greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d", size = 277747, upload-time = "2026-02-20T20:16:21.325Z" }, + { url = "https://files.pythonhosted.org/packages/fb/07/cb284a8b5c6498dbd7cba35d31380bb123d7dceaa7907f606c8ff5993cbf/greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13", size = 579202, upload-time = "2026-02-20T20:47:28.955Z" }, + { url = "https://files.pythonhosted.org/packages/ed/45/67922992b3a152f726163b19f890a85129a992f39607a2a53155de3448b8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e", size = 590620, upload-time = "2026-02-20T20:55:55.581Z" }, + { url = "https://files.pythonhosted.org/packages/03/5f/6e2a7d80c353587751ef3d44bb947f0565ec008a2e0927821c007e96d3a7/greenlet-3.3.2-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508c7f01f1791fbc8e011bd508f6794cb95397fdb198a46cb6635eb5b78d85a7", size = 602132, upload-time = "2026-02-20T21:02:43.261Z" }, + { url = "https://files.pythonhosted.org/packages/ad/55/9f1ebb5a825215fadcc0f7d5073f6e79e3007e3282b14b22d6aba7ca6cb8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f", size = 591729, upload-time = "2026-02-20T20:20:58.395Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/21f5455773d37f94b866eb3cf5caed88d6cea6dd2c6e1f9c34f463cba3ec/greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef", size = 1551946, upload-time = "2026-02-20T20:49:31.102Z" }, + { url = "https://files.pythonhosted.org/packages/00/68/91f061a926abead128fe1a87f0b453ccf07368666bd59ffa46016627a930/greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca", size = 1618494, upload-time = "2026-02-20T20:21:06.541Z" }, + { url = "https://files.pythonhosted.org/packages/ac/78/f93e840cbaef8becaf6adafbaf1319682a6c2d8c1c20224267a5c6c8c891/greenlet-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:5d0e35379f93a6d0222de929a25ab47b5eb35b5ef4721c2b9cbcc4036129ff1f", size = 230092, upload-time = "2026-02-20T20:17:09.379Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" }, + { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8b/1430a04657735a3f23116c2e0d5eb10220928846e4537a938a41b350bed6/greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2", size = 605046, upload-time = "2026-02-20T21:02:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" }, + { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" }, + { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5", size = 230389, upload-time = "2026-02-20T20:17:18.772Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a1/65bbc059a43a7e2143ec4fc1f9e3f673e04f9c7b371a494a101422ac4fd5/greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd", size = 229645, upload-time = "2026-02-20T20:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" }, + { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, + { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, + { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, + { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, + { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, + { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -2310,6 +2450,256 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "mmh3" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/af/f28c2c2f51f31abb4725f9a64bc7863d5f491f6539bd26aee2a1d21a649e/mmh3-5.2.0.tar.gz", hash = "sha256:1efc8fec8478e9243a78bb993422cf79f8ff85cb4cf6b79647480a31e0d950a8", size = 33582, upload-time = "2025-07-29T07:43:48.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/2b/870f0ff5ecf312c58500f45950751f214b7068665e66e9bfd8bc2595587c/mmh3-5.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:81c504ad11c588c8629536b032940f2a359dda3b6cbfd4ad8f74cb24dcd1b0bc", size = 56119, upload-time = "2025-07-29T07:41:39.117Z" }, + { url = "https://files.pythonhosted.org/packages/3b/88/eb9a55b3f3cf43a74d6bfa8db0e2e209f966007777a1dc897c52c008314c/mmh3-5.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b898cecff57442724a0f52bf42c2de42de63083a91008fb452887e372f9c328", size = 40634, upload-time = "2025-07-29T07:41:40.626Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4c/8e4b3878bf8435c697d7ce99940a3784eb864521768069feaccaff884a17/mmh3-5.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be1374df449465c9f2500e62eee73a39db62152a8bdfbe12ec5b5c1cd451344d", size = 40080, upload-time = "2025-07-29T07:41:41.791Z" }, + { url = "https://files.pythonhosted.org/packages/45/ac/0a254402c8c5ca424a0a9ebfe870f5665922f932830f0a11a517b6390a09/mmh3-5.2.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0d753ad566c721faa33db7e2e0eddd74b224cdd3eaf8481d76c926603c7a00e", size = 95321, upload-time = "2025-07-29T07:41:42.659Z" }, + { url = "https://files.pythonhosted.org/packages/39/8e/29306d5eca6dfda4b899d22c95b5420db4e0ffb7e0b6389b17379654ece5/mmh3-5.2.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dfbead5575f6470c17e955b94f92d62a03dfc3d07f2e6f817d9b93dc211a1515", size = 101220, upload-time = "2025-07-29T07:41:43.572Z" }, + { url = "https://files.pythonhosted.org/packages/49/f7/0dd1368e531e52a17b5b8dd2f379cce813bff2d0978a7748a506f1231152/mmh3-5.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7434a27754049144539d2099a6d2da5d88b8bdeedf935180bf42ad59b3607aa3", size = 103991, upload-time = "2025-07-29T07:41:44.914Z" }, + { url = "https://files.pythonhosted.org/packages/35/06/abc7122c40f4abbfcef01d2dac6ec0b77ede9757e5be8b8a40a6265b1274/mmh3-5.2.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cadc16e8ea64b5d9a47363013e2bea469e121e6e7cb416a7593aeb24f2ad122e", size = 110894, upload-time = "2025-07-29T07:41:45.849Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2f/837885759afa4baccb8e40456e1cf76a4f3eac835b878c727ae1286c5f82/mmh3-5.2.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d765058da196f68dc721116cab335e696e87e76720e6ef8ee5a24801af65e63d", size = 118327, upload-time = "2025-07-29T07:41:47.224Z" }, + { url = "https://files.pythonhosted.org/packages/40/cc/5683ba20a21bcfb3f1605b1c474f46d30354f728a7412201f59f453d405a/mmh3-5.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8b0c53fe0994beade1ad7c0f13bd6fec980a0664bfbe5a6a7d64500b9ab76772", size = 101701, upload-time = "2025-07-29T07:41:48.259Z" }, + { url = "https://files.pythonhosted.org/packages/0e/24/99ab3fb940150aec8a26dbdfc39b200b5592f6aeb293ec268df93e054c30/mmh3-5.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:49037d417419863b222ae47ee562b2de9c3416add0a45c8d7f4e864be8dc4f89", size = 96712, upload-time = "2025-07-29T07:41:49.467Z" }, + { url = "https://files.pythonhosted.org/packages/61/04/d7c4cb18f1f001ede2e8aed0f9dbbfad03d161c9eea4fffb03f14f4523e5/mmh3-5.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6ecb4e750d712abde046858ee6992b65c93f1f71b397fce7975c3860c07365d2", size = 110302, upload-time = "2025-07-29T07:41:50.387Z" }, + { url = "https://files.pythonhosted.org/packages/d8/bf/4dac37580cfda74425a4547500c36fa13ef581c8a756727c37af45e11e9a/mmh3-5.2.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:382a6bb3f8c6532ea084e7acc5be6ae0c6effa529240836d59352398f002e3fc", size = 111929, upload-time = "2025-07-29T07:41:51.348Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b1/49f0a582c7a942fb71ddd1ec52b7d21d2544b37d2b2d994551346a15b4f6/mmh3-5.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7733ec52296fc1ba22e9b90a245c821adbb943e98c91d8a330a2254612726106", size = 100111, upload-time = "2025-07-29T07:41:53.139Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/ccec09f438caeb2506f4c63bb3b99aa08a9e09880f8fc047295154756210/mmh3-5.2.0-cp310-cp310-win32.whl", hash = "sha256:127c95336f2a98c51e7682341ab7cb0be3adb9df0819ab8505a726ed1801876d", size = 40783, upload-time = "2025-07-29T07:41:54.463Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f4/8d39a32c8203c1cdae88fdb04d1ea4aa178c20f159df97f4c5a2eaec702c/mmh3-5.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:419005f84ba1cab47a77465a2a843562dadadd6671b8758bf179d82a15ca63eb", size = 41549, upload-time = "2025-07-29T07:41:55.295Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a1/30efb1cd945e193f62574144dd92a0c9ee6463435e4e8ffce9b9e9f032f0/mmh3-5.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:d22c9dcafed659fadc605538946c041722b6d1104fe619dbf5cc73b3c8a0ded8", size = 39335, upload-time = "2025-07-29T07:41:56.194Z" }, + { url = "https://files.pythonhosted.org/packages/f7/87/399567b3796e134352e11a8b973cd470c06b2ecfad5468fe580833be442b/mmh3-5.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7901c893e704ee3c65f92d39b951f8f34ccf8e8566768c58103fb10e55afb8c1", size = 56107, upload-time = "2025-07-29T07:41:57.07Z" }, + { url = "https://files.pythonhosted.org/packages/c3/09/830af30adf8678955b247d97d3d9543dd2fd95684f3cd41c0cd9d291da9f/mmh3-5.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5f5536b1cbfa72318ab3bfc8a8188b949260baed186b75f0abc75b95d8c051", size = 40635, upload-time = "2025-07-29T07:41:57.903Z" }, + { url = "https://files.pythonhosted.org/packages/07/14/eaba79eef55b40d653321765ac5e8f6c9ac38780b8a7c2a2f8df8ee0fb72/mmh3-5.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cedac4f4054b8f7859e5aed41aaa31ad03fce6851901a7fdc2af0275ac533c10", size = 40078, upload-time = "2025-07-29T07:41:58.772Z" }, + { url = "https://files.pythonhosted.org/packages/bb/26/83a0f852e763f81b2265d446b13ed6d49ee49e1fc0c47b9655977e6f3d81/mmh3-5.2.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eb756caf8975882630ce4e9fbbeb9d3401242a72528230422c9ab3a0d278e60c", size = 97262, upload-time = "2025-07-29T07:41:59.678Z" }, + { url = "https://files.pythonhosted.org/packages/00/7d/b7133b10d12239aeaebf6878d7eaf0bf7d3738c44b4aba3c564588f6d802/mmh3-5.2.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:097e13c8b8a66c5753c6968b7640faefe85d8e38992703c1f666eda6ef4c3762", size = 103118, upload-time = "2025-07-29T07:42:01.197Z" }, + { url = "https://files.pythonhosted.org/packages/7b/3e/62f0b5dce2e22fd5b7d092aba285abd7959ea2b17148641e029f2eab1ffa/mmh3-5.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7c0c7845566b9686480e6a7e9044db4afb60038d5fabd19227443f0104eeee4", size = 106072, upload-time = "2025-07-29T07:42:02.601Z" }, + { url = "https://files.pythonhosted.org/packages/66/84/ea88bb816edfe65052c757a1c3408d65c4201ddbd769d4a287b0f1a628b2/mmh3-5.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:61ac226af521a572700f863d6ecddc6ece97220ce7174e311948ff8c8919a363", size = 112925, upload-time = "2025-07-29T07:42:03.632Z" }, + { url = "https://files.pythonhosted.org/packages/2e/13/c9b1c022807db575fe4db806f442d5b5784547e2e82cff36133e58ea31c7/mmh3-5.2.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:582f9dbeefe15c32a5fa528b79b088b599a1dfe290a4436351c6090f90ddebb8", size = 120583, upload-time = "2025-07-29T07:42:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5f/0e2dfe1a38f6a78788b7eb2b23432cee24623aeabbc907fed07fc17d6935/mmh3-5.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ebfc46b39168ab1cd44670a32ea5489bcbc74a25795c61b6d888c5c2cf654ed", size = 99127, upload-time = "2025-07-29T07:42:05.929Z" }, + { url = "https://files.pythonhosted.org/packages/77/27/aefb7d663b67e6a0c4d61a513c83e39ba2237e8e4557fa7122a742a23de5/mmh3-5.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1556e31e4bd0ac0c17eaf220be17a09c171d7396919c3794274cb3415a9d3646", size = 98544, upload-time = "2025-07-29T07:42:06.87Z" }, + { url = "https://files.pythonhosted.org/packages/ab/97/a21cc9b1a7c6e92205a1b5fa030cdf62277d177570c06a239eca7bd6dd32/mmh3-5.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:81df0dae22cd0da87f1c978602750f33d17fb3d21fb0f326c89dc89834fea79b", size = 106262, upload-time = "2025-07-29T07:42:07.804Z" }, + { url = "https://files.pythonhosted.org/packages/43/18/db19ae82ea63c8922a880e1498a75342311f8aa0c581c4dd07711473b5f7/mmh3-5.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:eba01ec3bd4a49b9ac5ca2bc6a73ff5f3af53374b8556fcc2966dd2af9eb7779", size = 109824, upload-time = "2025-07-29T07:42:08.735Z" }, + { url = "https://files.pythonhosted.org/packages/9f/f5/41dcf0d1969125fc6f61d8618b107c79130b5af50b18a4651210ea52ab40/mmh3-5.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e9a011469b47b752e7d20de296bb34591cdfcbe76c99c2e863ceaa2aa61113d2", size = 97255, upload-time = "2025-07-29T07:42:09.706Z" }, + { url = "https://files.pythonhosted.org/packages/32/b3/cce9eaa0efac1f0e735bb178ef9d1d2887b4927fe0ec16609d5acd492dda/mmh3-5.2.0-cp311-cp311-win32.whl", hash = "sha256:bc44fc2b886243d7c0d8daeb37864e16f232e5b56aaec27cc781d848264cfd28", size = 40779, upload-time = "2025-07-29T07:42:10.546Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e9/3fa0290122e6d5a7041b50ae500b8a9f4932478a51e48f209a3879fe0b9b/mmh3-5.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:8ebf241072cf2777a492d0e09252f8cc2b3edd07dfdb9404b9757bffeb4f2cee", size = 41549, upload-time = "2025-07-29T07:42:11.399Z" }, + { url = "https://files.pythonhosted.org/packages/3a/54/c277475b4102588e6f06b2e9095ee758dfe31a149312cdbf62d39a9f5c30/mmh3-5.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:b5f317a727bba0e633a12e71228bc6a4acb4f471a98b1c003163b917311ea9a9", size = 39336, upload-time = "2025-07-29T07:42:12.209Z" }, + { url = "https://files.pythonhosted.org/packages/bf/6a/d5aa7edb5c08e0bd24286c7d08341a0446f9a2fbbb97d96a8a6dd81935ee/mmh3-5.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:384eda9361a7bf83a85e09447e1feafe081034af9dd428893701b959230d84be", size = 56141, upload-time = "2025-07-29T07:42:13.456Z" }, + { url = "https://files.pythonhosted.org/packages/08/49/131d0fae6447bc4a7299ebdb1a6fb9d08c9f8dcf97d75ea93e8152ddf7ab/mmh3-5.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c9da0d568569cc87315cb063486d761e38458b8ad513fedd3dc9263e1b81bcd", size = 40681, upload-time = "2025-07-29T07:42:14.306Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6f/9221445a6bcc962b7f5ff3ba18ad55bba624bacdc7aa3fc0a518db7da8ec/mmh3-5.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86d1be5d63232e6eb93c50881aea55ff06eb86d8e08f9b5417c8c9b10db9db96", size = 40062, upload-time = "2025-07-29T07:42:15.08Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d4/6bb2d0fef81401e0bb4c297d1eb568b767de4ce6fc00890bc14d7b51ecc4/mmh3-5.2.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bf7bee43e17e81671c447e9c83499f53d99bf440bc6d9dc26a841e21acfbe094", size = 97333, upload-time = "2025-07-29T07:42:16.436Z" }, + { url = "https://files.pythonhosted.org/packages/44/e0/ccf0daff8134efbb4fbc10a945ab53302e358c4b016ada9bf97a6bdd50c1/mmh3-5.2.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7aa18cdb58983ee660c9c400b46272e14fa253c675ed963d3812487f8ca42037", size = 103310, upload-time = "2025-07-29T07:42:17.796Z" }, + { url = "https://files.pythonhosted.org/packages/02/63/1965cb08a46533faca0e420e06aff8bbaf9690a6f0ac6ae6e5b2e4544687/mmh3-5.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9d032488fcec32d22be6542d1a836f00247f40f320844dbb361393b5b22773", size = 106178, upload-time = "2025-07-29T07:42:19.281Z" }, + { url = "https://files.pythonhosted.org/packages/c2/41/c883ad8e2c234013f27f92061200afc11554ea55edd1bcf5e1accd803a85/mmh3-5.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1861fb6b1d0453ed7293200139c0a9011eeb1376632e048e3766945b13313c5", size = 113035, upload-time = "2025-07-29T07:42:20.356Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/1ccade8b1fa625d634a18bab7bf08a87457e09d5ec8cf83ca07cbea9d400/mmh3-5.2.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:99bb6a4d809aa4e528ddfe2c85dd5239b78b9dd14be62cca0329db78505e7b50", size = 120784, upload-time = "2025-07-29T07:42:21.377Z" }, + { url = "https://files.pythonhosted.org/packages/77/1c/919d9171fcbdcdab242e06394464ccf546f7d0f3b31e0d1e3a630398782e/mmh3-5.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1f8d8b627799f4e2fcc7c034fed8f5f24dc7724ff52f69838a3d6d15f1ad4765", size = 99137, upload-time = "2025-07-29T07:42:22.344Z" }, + { url = "https://files.pythonhosted.org/packages/66/8a/1eebef5bd6633d36281d9fc83cf2e9ba1ba0e1a77dff92aacab83001cee4/mmh3-5.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b5995088dd7023d2d9f310a0c67de5a2b2e06a570ecfd00f9ff4ab94a67cde43", size = 98664, upload-time = "2025-07-29T07:42:23.269Z" }, + { url = "https://files.pythonhosted.org/packages/13/41/a5d981563e2ee682b21fb65e29cc0f517a6734a02b581359edd67f9d0360/mmh3-5.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1a5f4d2e59d6bba8ef01b013c472741835ad961e7c28f50c82b27c57748744a4", size = 106459, upload-time = "2025-07-29T07:42:24.238Z" }, + { url = "https://files.pythonhosted.org/packages/24/31/342494cd6ab792d81e083680875a2c50fa0c5df475ebf0b67784f13e4647/mmh3-5.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fd6e6c3d90660d085f7e73710eab6f5545d4854b81b0135a3526e797009dbda3", size = 110038, upload-time = "2025-07-29T07:42:25.629Z" }, + { url = "https://files.pythonhosted.org/packages/28/44/efda282170a46bb4f19c3e2b90536513b1d821c414c28469a227ca5a1789/mmh3-5.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c4a2f3d83879e3de2eb8cbf562e71563a8ed15ee9b9c2e77ca5d9f73072ac15c", size = 97545, upload-time = "2025-07-29T07:42:27.04Z" }, + { url = "https://files.pythonhosted.org/packages/68/8f/534ae319c6e05d714f437e7206f78c17e66daca88164dff70286b0e8ea0c/mmh3-5.2.0-cp312-cp312-win32.whl", hash = "sha256:2421b9d665a0b1ad724ec7332fb5a98d075f50bc51a6ff854f3a1882bd650d49", size = 40805, upload-time = "2025-07-29T07:42:28.032Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f6/f6abdcfefcedab3c964868048cfe472764ed358c2bf6819a70dd4ed4ed3a/mmh3-5.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d80005b7634a3a2220f81fbeb94775ebd12794623bb2e1451701ea732b4aa3", size = 41597, upload-time = "2025-07-29T07:42:28.894Z" }, + { url = "https://files.pythonhosted.org/packages/15/fd/f7420e8cbce45c259c770cac5718badf907b302d3a99ec587ba5ce030237/mmh3-5.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:3d6bfd9662a20c054bc216f861fa330c2dac7c81e7fb8307b5e32ab5b9b4d2e0", size = 39350, upload-time = "2025-07-29T07:42:29.794Z" }, + { url = "https://files.pythonhosted.org/packages/d8/fa/27f6ab93995ef6ad9f940e96593c5dd24744d61a7389532b0fec03745607/mmh3-5.2.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:e79c00eba78f7258e5b354eccd4d7907d60317ced924ea4a5f2e9d83f5453065", size = 40874, upload-time = "2025-07-29T07:42:30.662Z" }, + { url = "https://files.pythonhosted.org/packages/11/9c/03d13bcb6a03438bc8cac3d2e50f80908d159b31a4367c2e1a7a077ded32/mmh3-5.2.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:956127e663d05edbeec54df38885d943dfa27406594c411139690485128525de", size = 42012, upload-time = "2025-07-29T07:42:31.539Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/0865d9765408a7d504f1789944e678f74e0888b96a766d578cb80b040999/mmh3-5.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c3dca4cb5b946ee91b3d6bb700d137b1cd85c20827f89fdf9c16258253489044", size = 39197, upload-time = "2025-07-29T07:42:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/3e/12/76c3207bd186f98b908b6706c2317abb73756d23a4e68ea2bc94825b9015/mmh3-5.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e651e17bfde5840e9e4174b01e9e080ce49277b70d424308b36a7969d0d1af73", size = 39840, upload-time = "2025-07-29T07:42:33.227Z" }, + { url = "https://files.pythonhosted.org/packages/5d/0d/574b6cce5555c9f2b31ea189ad44986755eb14e8862db28c8b834b8b64dc/mmh3-5.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:9f64bf06f4bf623325fda3a6d02d36cd69199b9ace99b04bb2d7fd9f89688504", size = 40644, upload-time = "2025-07-29T07:42:34.099Z" }, + { url = "https://files.pythonhosted.org/packages/52/82/3731f8640b79c46707f53ed72034a58baad400be908c87b0088f1f89f986/mmh3-5.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ddc63328889bcaee77b743309e5c7d2d52cee0d7d577837c91b6e7cc9e755e0b", size = 56153, upload-time = "2025-07-29T07:42:35.031Z" }, + { url = "https://files.pythonhosted.org/packages/4f/34/e02dca1d4727fd9fdeaff9e2ad6983e1552804ce1d92cc796e5b052159bb/mmh3-5.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bb0fdc451fb6d86d81ab8f23d881b8d6e37fc373a2deae1c02d27002d2ad7a05", size = 40684, upload-time = "2025-07-29T07:42:35.914Z" }, + { url = "https://files.pythonhosted.org/packages/8f/36/3dee40767356e104967e6ed6d102ba47b0b1ce2a89432239b95a94de1b89/mmh3-5.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b29044e1ffdb84fe164d0a7ea05c7316afea93c00f8ed9449cf357c36fc4f814", size = 40057, upload-time = "2025-07-29T07:42:36.755Z" }, + { url = "https://files.pythonhosted.org/packages/31/58/228c402fccf76eb39a0a01b8fc470fecf21965584e66453b477050ee0e99/mmh3-5.2.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:58981d6ea9646dbbf9e59a30890cbf9f610df0e4a57dbfe09215116fd90b0093", size = 97344, upload-time = "2025-07-29T07:42:37.675Z" }, + { url = "https://files.pythonhosted.org/packages/34/82/fc5ce89006389a6426ef28e326fc065b0fbaaed230373b62d14c889f47ea/mmh3-5.2.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e5634565367b6d98dc4aa2983703526ef556b3688ba3065edb4b9b90ede1c54", size = 103325, upload-time = "2025-07-29T07:42:38.591Z" }, + { url = "https://files.pythonhosted.org/packages/09/8c/261e85777c6aee1ebd53f2f17e210e7481d5b0846cd0b4a5c45f1e3761b8/mmh3-5.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0271ac12415afd3171ab9a3c7cbfc71dee2c68760a7dc9d05bf8ed6ddfa3a7a", size = 106240, upload-time = "2025-07-29T07:42:39.563Z" }, + { url = "https://files.pythonhosted.org/packages/70/73/2f76b3ad8a3d431824e9934403df36c0ddacc7831acf82114bce3c4309c8/mmh3-5.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:45b590e31bc552c6f8e2150ff1ad0c28dd151e9f87589e7eaf508fbdd8e8e908", size = 113060, upload-time = "2025-07-29T07:42:40.585Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b9/7ea61a34e90e50a79a9d87aa1c0b8139a7eaf4125782b34b7d7383472633/mmh3-5.2.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bdde97310d59604f2a9119322f61b31546748499a21b44f6715e8ced9308a6c5", size = 120781, upload-time = "2025-07-29T07:42:41.618Z" }, + { url = "https://files.pythonhosted.org/packages/0f/5b/ae1a717db98c7894a37aeedbd94b3f99e6472a836488f36b6849d003485b/mmh3-5.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc9c5f280438cf1c1a8f9abb87dc8ce9630a964120cfb5dd50d1e7ce79690c7a", size = 99174, upload-time = "2025-07-29T07:42:42.587Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/000cce1d799fceebb6d4487ae29175dd8e81b48e314cba7b4da90bcf55d7/mmh3-5.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c903e71fd8debb35ad2a4184c1316b3cb22f64ce517b4e6747f25b0a34e41266", size = 98734, upload-time = "2025-07-29T07:42:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/79/19/0dc364391a792b72fbb22becfdeacc5add85cc043cd16986e82152141883/mmh3-5.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:eed4bba7ff8a0d37106ba931ab03bdd3915fbb025bcf4e1f0aa02bc8114960c5", size = 106493, upload-time = "2025-07-29T07:42:45.07Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b1/bc8c28e4d6e807bbb051fefe78e1156d7f104b89948742ad310612ce240d/mmh3-5.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1fdb36b940e9261aff0b5177c5b74a36936b902f473180f6c15bde26143681a9", size = 110089, upload-time = "2025-07-29T07:42:46.122Z" }, + { url = "https://files.pythonhosted.org/packages/3b/a2/d20f3f5c95e9c511806686c70d0a15479cc3941c5f322061697af1c1ff70/mmh3-5.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7303aab41e97adcf010a09efd8f1403e719e59b7705d5e3cfed3dd7571589290", size = 97571, upload-time = "2025-07-29T07:42:47.18Z" }, + { url = "https://files.pythonhosted.org/packages/7b/23/665296fce4f33488deec39a750ffd245cfc07aafb0e3ef37835f91775d14/mmh3-5.2.0-cp313-cp313-win32.whl", hash = "sha256:03e08c6ebaf666ec1e3d6ea657a2d363bb01effd1a9acfe41f9197decaef0051", size = 40806, upload-time = "2025-07-29T07:42:48.166Z" }, + { url = "https://files.pythonhosted.org/packages/59/b0/92e7103f3b20646e255b699e2d0327ce53a3f250e44367a99dc8be0b7c7a/mmh3-5.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:7fddccd4113e7b736706e17a239a696332360cbaddf25ae75b57ba1acce65081", size = 41600, upload-time = "2025-07-29T07:42:49.371Z" }, + { url = "https://files.pythonhosted.org/packages/99/22/0b2bd679a84574647de538c5b07ccaa435dbccc37815067fe15b90fe8dad/mmh3-5.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa0c966ee727aad5406d516375593c5f058c766b21236ab8985693934bb5085b", size = 39349, upload-time = "2025-07-29T07:42:50.268Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ca/a20db059a8a47048aaf550da14a145b56e9c7386fb8280d3ce2962dcebf7/mmh3-5.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e5015f0bb6eb50008bed2d4b1ce0f2a294698a926111e4bb202c0987b4f89078", size = 39209, upload-time = "2025-07-29T07:42:51.559Z" }, + { url = "https://files.pythonhosted.org/packages/98/dd/e5094799d55c7482d814b979a0fd608027d0af1b274bfb4c3ea3e950bfd5/mmh3-5.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e0f3ed828d709f5b82d8bfe14f8856120718ec4bd44a5b26102c3030a1e12501", size = 39843, upload-time = "2025-07-29T07:42:52.536Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6b/7844d7f832c85400e7cc89a1348e4e1fdd38c5a38415bb5726bbb8fcdb6c/mmh3-5.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:f35727c5118aba95f0397e18a1a5b8405425581bfe53e821f0fb444cbdc2bc9b", size = 40648, upload-time = "2025-07-29T07:42:53.392Z" }, + { url = "https://files.pythonhosted.org/packages/1f/bf/71f791f48a21ff3190ba5225807cbe4f7223360e96862c376e6e3fb7efa7/mmh3-5.2.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bc244802ccab5220008cb712ca1508cb6a12f0eb64ad62997156410579a1770", size = 56164, upload-time = "2025-07-29T07:42:54.267Z" }, + { url = "https://files.pythonhosted.org/packages/70/1f/f87e3d34d83032b4f3f0f528c6d95a98290fcacf019da61343a49dccfd51/mmh3-5.2.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ff3d50dc3fe8a98059f99b445dfb62792b5d006c5e0b8f03c6de2813b8376110", size = 40692, upload-time = "2025-07-29T07:42:55.234Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e2/db849eaed07117086f3452feca8c839d30d38b830ac59fe1ce65af8be5ad/mmh3-5.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:37a358cc881fe796e099c1db6ce07ff757f088827b4e8467ac52b7a7ffdca647", size = 40068, upload-time = "2025-07-29T07:42:56.158Z" }, + { url = "https://files.pythonhosted.org/packages/df/6b/209af927207af77425b044e32f77f49105a0b05d82ff88af6971d8da4e19/mmh3-5.2.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b9a87025121d1c448f24f27ff53a5fe7b6ef980574b4a4f11acaabe702420d63", size = 97367, upload-time = "2025-07-29T07:42:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e0/78adf4104c425606a9ce33fb351f790c76a6c2314969c4a517d1ffc92196/mmh3-5.2.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ba55d6ca32eeef8b2625e1e4bfc3b3db52bc63014bd7e5df8cc11bf2b036b12", size = 103306, upload-time = "2025-07-29T07:42:58.522Z" }, + { url = "https://files.pythonhosted.org/packages/a3/79/c2b89f91b962658b890104745b1b6c9ce38d50a889f000b469b91eeb1b9e/mmh3-5.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9ff37ba9f15637e424c2ab57a1a590c52897c845b768e4e0a4958084ec87f22", size = 106312, upload-time = "2025-07-29T07:42:59.552Z" }, + { url = "https://files.pythonhosted.org/packages/4b/14/659d4095528b1a209be90934778c5ffe312177d51e365ddcbca2cac2ec7c/mmh3-5.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a094319ec0db52a04af9fdc391b4d39a1bc72bc8424b47c4411afb05413a44b5", size = 113135, upload-time = "2025-07-29T07:43:00.745Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6f/cd7734a779389a8a467b5c89a48ff476d6f2576e78216a37551a97e9e42a/mmh3-5.2.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c5584061fd3da584659b13587f26c6cad25a096246a481636d64375d0c1f6c07", size = 120775, upload-time = "2025-07-29T07:43:02.124Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ca/8256e3b96944408940de3f9291d7e38a283b5761fe9614d4808fcf27bd62/mmh3-5.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecbfc0437ddfdced5e7822d1ce4855c9c64f46819d0fdc4482c53f56c707b935", size = 99178, upload-time = "2025-07-29T07:43:03.182Z" }, + { url = "https://files.pythonhosted.org/packages/8a/32/39e2b3cf06b6e2eb042c984dab8680841ac2a0d3ca6e0bea30db1f27b565/mmh3-5.2.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7b986d506a8e8ea345791897ba5d8ba0d9d8820cd4fc3e52dbe6de19388de2e7", size = 98738, upload-time = "2025-07-29T07:43:04.207Z" }, + { url = "https://files.pythonhosted.org/packages/61/d3/7bbc8e0e8cf65ebbe1b893ffa0467b7ecd1bd07c3bbf6c9db4308ada22ec/mmh3-5.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:38d899a156549da8ef6a9f1d6f7ef231228d29f8f69bce2ee12f5fba6d6fd7c5", size = 106510, upload-time = "2025-07-29T07:43:05.656Z" }, + { url = "https://files.pythonhosted.org/packages/10/99/b97e53724b52374e2f3859046f0eb2425192da356cb19784d64bc17bb1cf/mmh3-5.2.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d86651fa45799530885ba4dab3d21144486ed15285e8784181a0ab37a4552384", size = 110053, upload-time = "2025-07-29T07:43:07.204Z" }, + { url = "https://files.pythonhosted.org/packages/ac/62/3688c7d975ed195155671df68788c83fed6f7909b6ec4951724c6860cb97/mmh3-5.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c463d7c1c4cfc9d751efeaadd936bbba07b5b0ed81a012b3a9f5a12f0872bd6e", size = 97546, upload-time = "2025-07-29T07:43:08.226Z" }, + { url = "https://files.pythonhosted.org/packages/ca/3b/c6153250f03f71a8b7634cded82939546cdfba02e32f124ff51d52c6f991/mmh3-5.2.0-cp314-cp314-win32.whl", hash = "sha256:bb4fe46bdc6104fbc28db7a6bacb115ee6368ff993366bbd8a2a7f0076e6f0c0", size = 41422, upload-time = "2025-07-29T07:43:09.216Z" }, + { url = "https://files.pythonhosted.org/packages/74/01/a27d98bab083a435c4c07e9d1d720d4c8a578bf4c270bae373760b1022be/mmh3-5.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c7f0b342fd06044bedd0b6e72177ddc0076f54fd89ee239447f8b271d919d9b", size = 42135, upload-time = "2025-07-29T07:43:10.183Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c9/dbba5507e95429b8b380e2ba091eff5c20a70a59560934dff0ad8392b8c8/mmh3-5.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:3193752fc05ea72366c2b63ff24b9a190f422e32d75fdeae71087c08fff26115", size = 39879, upload-time = "2025-07-29T07:43:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d1/c8c0ef839c17258b9de41b84f663574fabcf8ac2007b7416575e0f65ff6e/mmh3-5.2.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:69fc339d7202bea69ef9bd7c39bfdf9fdabc8e6822a01eba62fb43233c1b3932", size = 57696, upload-time = "2025-07-29T07:43:11.989Z" }, + { url = "https://files.pythonhosted.org/packages/2f/55/95e2b9ff201e89f9fe37036037ab61a6c941942b25cdb7b6a9df9b931993/mmh3-5.2.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:12da42c0a55c9d86ab566395324213c319c73ecb0c239fad4726324212b9441c", size = 41421, upload-time = "2025-07-29T07:43:13.269Z" }, + { url = "https://files.pythonhosted.org/packages/77/79/9be23ad0b7001a4b22752e7693be232428ecc0a35068a4ff5c2f14ef8b20/mmh3-5.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f7f9034c7cf05ddfaac8d7a2e63a3c97a840d4615d0a0e65ba8bdf6f8576e3be", size = 40853, upload-time = "2025-07-29T07:43:14.888Z" }, + { url = "https://files.pythonhosted.org/packages/ac/1b/96b32058eda1c1dee8264900c37c359a7325c1f11f5ff14fd2be8e24eff9/mmh3-5.2.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11730eeb16dfcf9674fdea9bb6b8e6dd9b40813b7eb839bc35113649eef38aeb", size = 109694, upload-time = "2025-07-29T07:43:15.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6f/a2ae44cd7dad697b6dea48390cbc977b1e5ca58fda09628cbcb2275af064/mmh3-5.2.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:932a6eec1d2e2c3c9e630d10f7128d80e70e2d47fe6b8c7ea5e1afbd98733e65", size = 117438, upload-time = "2025-07-29T07:43:16.865Z" }, + { url = "https://files.pythonhosted.org/packages/a0/08/bfb75451c83f05224a28afeaf3950c7b793c0b71440d571f8e819cfb149a/mmh3-5.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ca975c51c5028947bbcfc24966517aac06a01d6c921e30f7c5383c195f87991", size = 120409, upload-time = "2025-07-29T07:43:18.207Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ea/8b118b69b2ff8df568f742387d1a159bc654a0f78741b31437dd047ea28e/mmh3-5.2.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5b0b58215befe0f0e120b828f7645e97719bbba9f23b69e268ed0ac7adde8645", size = 125909, upload-time = "2025-07-29T07:43:19.39Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/168cc0b6a30650032e351a3b89b8a47382da541993a03af91e1ba2501234/mmh3-5.2.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29c2b9ce61886809d0492a274a5a53047742dea0f703f9c4d5d223c3ea6377d3", size = 135331, upload-time = "2025-07-29T07:43:20.435Z" }, + { url = "https://files.pythonhosted.org/packages/31/05/e3a9849b1c18a7934c64e831492c99e67daebe84a8c2f2c39a7096a830e3/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a367d4741ac0103f8198c82f429bccb9359f543ca542b06a51f4f0332e8de279", size = 110085, upload-time = "2025-07-29T07:43:21.92Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d5/a96bcc306e3404601418b2a9a370baec92af84204528ba659fdfe34c242f/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5a5dba98e514fb26241868f6eb90a7f7ca0e039aed779342965ce24ea32ba513", size = 111195, upload-time = "2025-07-29T07:43:23.066Z" }, + { url = "https://files.pythonhosted.org/packages/af/29/0fd49801fec5bff37198684e0849b58e0dab3a2a68382a357cfffb0fafc3/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:941603bfd75a46023807511c1ac2f1b0f39cccc393c15039969806063b27e6db", size = 116919, upload-time = "2025-07-29T07:43:24.178Z" }, + { url = "https://files.pythonhosted.org/packages/2d/04/4f3c32b0a2ed762edca45d8b46568fc3668e34f00fb1e0a3b5451ec1281c/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:132dd943451a7c7546978863d2f5a64977928410782e1a87d583cb60eb89e667", size = 123160, upload-time = "2025-07-29T07:43:25.26Z" }, + { url = "https://files.pythonhosted.org/packages/91/76/3d29eaa38821730633d6a240d36fa8ad2807e9dfd432c12e1a472ed211eb/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f698733a8a494466432d611a8f0d1e026f5286dee051beea4b3c3146817e35d5", size = 110206, upload-time = "2025-07-29T07:43:26.699Z" }, + { url = "https://files.pythonhosted.org/packages/44/1c/ccf35892684d3a408202e296e56843743e0b4fb1629e59432ea88cdb3909/mmh3-5.2.0-cp314-cp314t-win32.whl", hash = "sha256:6d541038b3fc360ec538fc116de87462627944765a6750308118f8b509a8eec7", size = 41970, upload-time = "2025-07-29T07:43:27.666Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/b9e4f1e5adb5e21eb104588fcee2cd1eaa8308255173481427d5ecc4284e/mmh3-5.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e912b19cf2378f2967d0c08e86ff4c6c360129887f678e27e4dde970d21b3f4d", size = 43063, upload-time = "2025-07-29T07:43:28.582Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0e61d9a4e29c8679356795a40e48f647b4aad58d71bfc969f0f8f56fb912/mmh3-5.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e7884931fe5e788163e7b3c511614130c2c59feffdc21112290a194487efb2e9", size = 40455, upload-time = "2025-07-29T07:43:29.563Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/4bad09e880b648eeb55393a644c08efbd7da302fc405c8d2f6555521bb98/mmh3-5.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3c6041fd9d5fb5fcac57d5c80f521a36b74aea06b8566431c63e4ffc49aced51", size = 56117, upload-time = "2025-07-29T07:43:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/b2/43/97cacd1fa2994b4ec110334388e126fe000ddf041829721e2e59e46b0a7c/mmh3-5.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:58477cf9ef16664d1ce2b038f87d2dc96d70fe50733a34a7f07da6c9a5e3538c", size = 40634, upload-time = "2025-07-29T07:43:31.917Z" }, + { url = "https://files.pythonhosted.org/packages/e9/03/2a52e464b0e23f9838267adf75f942c5addc2c1f009a48d1ef5c331084fb/mmh3-5.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:be7d3dca9358e01dab1bad881fb2b4e8730cec58d36dd44482bc068bfcd3bc65", size = 40075, upload-time = "2025-07-29T07:43:32.9Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d3/c0c00f7eb436a0adf64d8a877673ac76096bf86aca57b6a2c80786d69242/mmh3-5.2.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:931d47e08c9c8a67bf75d82f0ada8399eac18b03388818b62bfa42882d571d72", size = 95112, upload-time = "2025-07-29T07:43:33.815Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f3/116cc1171bcb41a9cec10c46ee1d8bb5185d70c15848ff66d15ab7afb6fd/mmh3-5.2.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dd966df3489ec13848d6c6303429bbace94a153f43d1ae2a55115fd36fd5ca5d", size = 101006, upload-time = "2025-07-29T07:43:34.876Z" }, + { url = "https://files.pythonhosted.org/packages/41/34/b38a0c5c323666e632cc07d4fd337c4af0b300619c7b8b7a1d9a2db1ac1a/mmh3-5.2.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c677d78887244bf3095020b73c42b505b700f801c690f8eaa90ad12d3179612f", size = 103782, upload-time = "2025-07-29T07:43:35.987Z" }, + { url = "https://files.pythonhosted.org/packages/25/d6/42b5ae7219ec87f756ffafcf7471b7fd3386e352653522d155f4897e06d0/mmh3-5.2.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63830f846797187c5d3e2dae50f0848fdc86032f5bfdc58ae352f02f857e9025", size = 110660, upload-time = "2025-07-29T07:43:37.103Z" }, + { url = "https://files.pythonhosted.org/packages/8f/55/daea1ee478328f7ed3b5422f080a3f892e02bc1542f0bc5a1be083a05758/mmh3-5.2.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c3f563e8901960e2eaa64c8e8821895818acabeb41c96f2efbb936f65dbe486c", size = 118107, upload-time = "2025-07-29T07:43:38.173Z" }, + { url = "https://files.pythonhosted.org/packages/46/f1/930d3395a0aaef49db41019e94a7b46ac35b9a64c213a620eacac34078c0/mmh3-5.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:96f1e1ac44cbb42bcc406e509f70c9af42c594e72ccc7b1257f97554204445f0", size = 101448, upload-time = "2025-07-29T07:43:39.199Z" }, + { url = "https://files.pythonhosted.org/packages/cc/e4/543bf2622a1645fa560c26fe5dc2919c8c9eb2f9ac129778ce6acc9848fc/mmh3-5.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:7bbb0df897944b5ec830f3ad883e32c5a7375370a521565f5fe24443bfb2c4f7", size = 96474, upload-time = "2025-07-29T07:43:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/16/d8/9c552bd64c86bb03fba08d4b702efd65b09ed54c6969df0d1ec7fa8c0ae4/mmh3-5.2.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:1fae471339ae1b9c641f19cf46dfe6ffd7f64b1fba7c4333b99fa3dd7f21ae0a", size = 110049, upload-time = "2025-07-29T07:43:42.106Z" }, + { url = "https://files.pythonhosted.org/packages/6b/47/8a012b9c4d9c9b704ffcd71cad861ef120b2bd417d081bdb3aaa9e396fe6/mmh3-5.2.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:aa6e5d31fdc5ed9e3e95f9873508615a778fe9b523d52c17fc770a3eb39ab6e4", size = 111683, upload-time = "2025-07-29T07:43:43.228Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fc/4ad1bd01976484d0568a7d18d5a8597da1e65e76ac763114573dcd09d225/mmh3-5.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:746a5ee71c6d1103d9b560fa147881b5e68fd35da56e54e03d5acefad0e7c055", size = 99883, upload-time = "2025-07-29T07:43:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/4fbd0f74c7e9c35f5f70eb77509b7a706ef76ee86957a79e228f47cf037f/mmh3-5.2.0-cp39-cp39-win32.whl", hash = "sha256:10983c10f5c77683bd845751905ba535ec47409874acc759d5ce3ff7ef34398a", size = 40790, upload-time = "2025-07-29T07:43:45.296Z" }, + { url = "https://files.pythonhosted.org/packages/a0/61/0f593606dbd3a4259301ffb61678433656dc4a2c6da022fa7a122de7ffb4/mmh3-5.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:fdfd3fb739f4e22746e13ad7ba0c6eedf5f454b18d11249724a388868e308ee4", size = 41563, upload-time = "2025-07-29T07:43:46.599Z" }, + { url = "https://files.pythonhosted.org/packages/07/e6/ff066b72d86f0a19d3e4b6f3af073a9a328cb3cb4b068e25972866fcd517/mmh3-5.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:33576136c06b46a7046b6d83a3d75fbca7d25f84cec743f1ae156362608dc6d2", size = 39340, upload-time = "2025-07-29T07:43:47.512Z" }, +] + +[[package]] +name = "mmh3" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/91/1a/edb23803a168f070ded7a3014c6d706f63b90c84ccc024f89d794a3b7a6d/mmh3-5.2.1.tar.gz", hash = "sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad", size = 33775, upload-time = "2026-03-05T15:55:57.716Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/bb/88ee54afa5644b0f35ab5b435f208394feb963e5bb47c4e404deb625ffa4/mmh3-5.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5d87a3584093e1a89987e3d36d82c98d9621b2cb944e22a420aa1401e096758f", size = 56080, upload-time = "2026-03-05T15:53:40.452Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bf/5404c2fd6ac84819e8ff1b7e34437b37cf55a2b11318894909e7bb88de3f/mmh3-5.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30e4d2084df019880d55f6f7bea35328d9b464ebee090baa372c096dc77556fb", size = 40462, upload-time = "2026-03-05T15:53:41.751Z" }, + { url = "https://files.pythonhosted.org/packages/de/0b/52bffad0b52ae4ea53e222b594bd38c08ecac1fc410323220a7202e43da5/mmh3-5.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bbc17250b10d3466875a40a52520a6bac3c02334ca709207648abd3c223ed5c", size = 40077, upload-time = "2026-03-05T15:53:42.753Z" }, + { url = "https://files.pythonhosted.org/packages/a0/9e/326c93d425b9fa4cbcdc71bc32aaba520db37577d632a24d25d927594eca/mmh3-5.2.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:76219cd1eefb9bf4af7856e3ae563d15158efa145c0aab01e9933051a1954045", size = 95302, upload-time = "2026-03-05T15:53:43.867Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b1/e20d5f0d19c4c0f3df213fa7dcfa0942c4fb127d38e11f398ae8ddf6cccc/mmh3-5.2.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb9d44c25244e11c8be3f12c938ca8ba8404620ef8092245d2093c6ab3df260f", size = 101174, upload-time = "2026-03-05T15:53:45.194Z" }, + { url = "https://files.pythonhosted.org/packages/7f/4a/1a9bb3e33c18b1e1cee2c249a3053c4d4d9c93ecb30738f39a62249a7e86/mmh3-5.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d5d542bf2abd0fd0361e8017d03f7cb5786214ceb4a40eef1539d6585d93386", size = 103979, upload-time = "2026-03-05T15:53:46.334Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/dab9ee7545429e7acdd38d23d0104471d31de09a0c695f1b751e0ff34532/mmh3-5.2.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:08043f7cb1fb9467c3fbbbaea7896986e7fbc81f4d3fd9289a73d9110ab6207a", size = 110898, upload-time = "2026-03-05T15:53:47.443Z" }, + { url = "https://files.pythonhosted.org/packages/72/08/408f11af7fe9e76b883142bb06536007cc7f237be2a5e9ad4e837716e627/mmh3-5.2.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:add7ac388d1e0bf57259afbcf9ed05621a3bf11ce5ee337e7536f1e1aaf056b0", size = 118308, upload-time = "2026-03-05T15:53:49.1Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/0551be7fe0000736d9ad12ffa1f130d7a0c17b49193d6dc41c82bd9404c6/mmh3-5.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41105377f6282e8297f182e393a79cfffd521dde37ace52b106373bdcd9ca5cb", size = 101671, upload-time = "2026-03-05T15:53:50.317Z" }, + { url = "https://files.pythonhosted.org/packages/44/17/6e4f80c4e6ad590139fa2017c3aeca54e7cc9ef68e08aa142a0c90f40a97/mmh3-5.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3cb61db880ec11e984348227b333259994c2c85caa775eb7875decb3768db890", size = 96682, upload-time = "2026-03-05T15:53:51.48Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a7/b82fccd38c1fa815de72e94ebe9874562964a10e21e6c1bc3b01d3f15a0e/mmh3-5.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e8b5378de2b139c3a830f0209c1e91f7705919a4b3e563a10955104f5097a70a", size = 110287, upload-time = "2026-03-05T15:53:52.68Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a1/2644069031c8cec0be46f0346f568a53f42fddd843f03cc890306699c1e2/mmh3-5.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e904f2417f0d6f6d514f3f8b836416c360f306ddaee1f84de8eef1e722d212e5", size = 111899, upload-time = "2026-03-05T15:53:53.791Z" }, + { url = "https://files.pythonhosted.org/packages/51/7b/6614f3eb8fb33f931fa7616c6d477247e48ec6c5082b02eeeee998cffa94/mmh3-5.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f1fbb0a99125b1287c6d9747f937dc66621426836d1a2d50d05aecfc81911b57", size = 100078, upload-time = "2026-03-05T15:53:55.234Z" }, + { url = "https://files.pythonhosted.org/packages/27/9a/dd4d5a5fb893e64f71b42b69ecae97dd78db35075412488b24036bc5599c/mmh3-5.2.1-cp310-cp310-win32.whl", hash = "sha256:b4cce60d0223074803c9dbe0721ad3fa51dafe7d462fee4b656a1aa01ee07518", size = 40756, upload-time = "2026-03-05T15:53:56.319Z" }, + { url = "https://files.pythonhosted.org/packages/c9/34/0b25889450f8aeffcec840aa73251e853f059c1b72ed1d1c027b956f95f5/mmh3-5.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:6f01f044112d43a20be2f13a11683666d87151542ad627fe41a18b9791d2802f", size = 41519, upload-time = "2026-03-05T15:53:57.41Z" }, + { url = "https://files.pythonhosted.org/packages/fd/31/8fd42e3c526d0bcb1db7f569c0de6729e180860a0495e387a53af33c2043/mmh3-5.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:7501e9be34cb21e72fcfe672aafd0eee65c16ba2afa9dcb5500a587d3a0580f0", size = 39285, upload-time = "2026-03-05T15:53:58.697Z" }, + { url = "https://files.pythonhosted.org/packages/65/d7/3312a59df3c1cdd783f4cf0c4ee8e9decff9c5466937182e4cc7dbbfe6c5/mmh3-5.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dae0f0bd7d30c0ad61b9a504e8e272cb8391eed3f1587edf933f4f6b33437450", size = 56082, upload-time = "2026-03-05T15:53:59.702Z" }, + { url = "https://files.pythonhosted.org/packages/61/96/6f617baa098ca0d2989bfec6d28b5719532cd8d8848782662f5b755f657f/mmh3-5.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9aeaf53eaa075dd63e81512522fd180097312fb2c9f476333309184285c49ce0", size = 40458, upload-time = "2026-03-05T15:54:01.548Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b4/9cd284bd6062d711e13d26c04d4778ab3f690c1c38a4563e3c767ec8802e/mmh3-5.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0634581290e6714c068f4aa24020acf7880927d1f0084fa753d9799ae9610082", size = 40079, upload-time = "2026-03-05T15:54:02.743Z" }, + { url = "https://files.pythonhosted.org/packages/f6/09/a806334ce1d3d50bf782b95fcee8b3648e1e170327d4bb7b4bad2ad7d956/mmh3-5.2.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080c0637aea036f35507e803a4778f119a9b436617694ae1c5c366805f1e997", size = 97242, upload-time = "2026-03-05T15:54:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/ee/93/723e317dd9e041c4dc4566a2eb53b01ad94de31750e0b834f1643905e97c/mmh3-5.2.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db0562c5f71d18596dcd45e854cf2eeba27d7543e1a3acdafb7eef728f7fe85d", size = 103082, upload-time = "2026-03-05T15:54:06.387Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/f96121e69cc48696075071531cf574f112e1ffd08059f4bffb41210e6fc5/mmh3-5.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d9f9a3ce559a5267014b04b82956993270f63ec91765e13e9fd73daf2d2738e", size = 106054, upload-time = "2026-03-05T15:54:07.506Z" }, + { url = "https://files.pythonhosted.org/packages/82/49/192b987ec48d0b2aecf8ac285a9b11fbc00030f6b9c694664ae923458dde/mmh3-5.2.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:960b1b3efa39872ac8b6cc3a556edd6fb90ed74f08c9c45e028f1005b26aa55d", size = 112910, upload-time = "2026-03-05T15:54:09.403Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a1/03e91fd334ed0144b83343a76eb11f17434cd08f746401488cfeafb2d241/mmh3-5.2.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d30b650595fdbe32366b94cb14f30bb2b625e512bd4e1df00611f99dc5c27fd4", size = 120551, upload-time = "2026-03-05T15:54:10.587Z" }, + { url = "https://files.pythonhosted.org/packages/93/b9/b89a71d2ff35c3a764d1c066c7313fc62c7cc48fa48a4b3b0304a4a0146f/mmh3-5.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82f3802bfc4751f420d591c5c864de538b71cea117fce67e4595c2afede08a15", size = 99096, upload-time = "2026-03-05T15:54:11.76Z" }, + { url = "https://files.pythonhosted.org/packages/36/b5/613772c1c6ed5f7b63df55eb131e887cc43720fec392777b95a79d34e640/mmh3-5.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:915e7a2418f10bd1151b1953df06d896db9783c9cfdb9a8ee1f9b3a4331ab503", size = 98524, upload-time = "2026-03-05T15:54:13.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0e/1524566fe8eaf871e4f7bc44095929fcd2620488f402822d848df19d679c/mmh3-5.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fc78739b5ec6e4fb02301984a3d442a91406e7700efbe305071e7fd1c78278f2", size = 106239, upload-time = "2026-03-05T15:54:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/04/94/21adfa7d90a7a697137ad6de33eeff6445420ca55e433a5d4919c79bc3b5/mmh3-5.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:41aac7002a749f08727cb91babff1daf8deac317c0b1f317adc69be0e6c375d1", size = 109797, upload-time = "2026-03-05T15:54:15.819Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e6/1aacc3a219e1aa62fa65669995d4a3562b35be5200ec03680c7e4bec9676/mmh3-5.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9d8089d853c7963a8ce87fff93e2a67075c0bc08684a08ea6ad13577c38ffc38", size = 97228, upload-time = "2026-03-05T15:54:16.992Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b9/5e4cca8dcccf298add0a27f3c357bc8cf8baf821d35cdc6165e4bd5a48b0/mmh3-5.2.1-cp311-cp311-win32.whl", hash = "sha256:baeb47635cb33375dee4924cd93d7f5dcaa786c740b08423b0209b824a1ee728", size = 40751, upload-time = "2026-03-05T15:54:18.714Z" }, + { url = "https://files.pythonhosted.org/packages/72/fc/5b11d49247f499bcda591171e9cf3b6ee422b19e70aa2cef2e0ae65ca3b9/mmh3-5.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:1e4ecee40ba19e6975e1120829796770325841c2f153c0e9aecca927194c6a2a", size = 41517, upload-time = "2026-03-05T15:54:19.764Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5f/2a511ee8a1c2a527c77726d5231685b72312c5a1a1b7639ad66a9652aa84/mmh3-5.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:c302245fd6c33d96bd169c7ccf2513c20f4c1e417c07ce9dce107c8bc3f8411f", size = 39287, upload-time = "2026-03-05T15:54:20.904Z" }, + { url = "https://files.pythonhosted.org/packages/92/94/bc5c3b573b40a328c4d141c20e399039ada95e5e2a661df3425c5165fd84/mmh3-5.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1", size = 56087, upload-time = "2026-03-05T15:54:21.92Z" }, + { url = "https://files.pythonhosted.org/packages/f6/80/64a02cc3e95c3af0aaa2590849d9ed24a9f14bb93537addde688e039b7c3/mmh3-5.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00", size = 40500, upload-time = "2026-03-05T15:54:22.953Z" }, + { url = "https://files.pythonhosted.org/packages/8b/72/e6d6602ce18adf4ddcd0e48f2e13590cc92a536199e52109f46f259d3c46/mmh3-5.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7", size = 40034, upload-time = "2026-03-05T15:54:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/59/c2/bf4537a8e58e21886ef16477041238cab5095c836496e19fafc34b7445d2/mmh3-5.2.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b", size = 97292, upload-time = "2026-03-05T15:54:25.335Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e2/51ed62063b44d10b06d975ac87af287729eeb5e3ed9772f7584a17983e90/mmh3-5.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006", size = 103274, upload-time = "2026-03-05T15:54:26.44Z" }, + { url = "https://files.pythonhosted.org/packages/75/ce/12a7524dca59eec92e5b31fdb13ede1e98eda277cf2b786cf73bfbc24e81/mmh3-5.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825", size = 106158, upload-time = "2026-03-05T15:54:28.578Z" }, + { url = "https://files.pythonhosted.org/packages/86/1f/d3ba6dd322d01ab5d44c46c8f0c38ab6bbbf9b5e20e666dfc05bf4a23604/mmh3-5.2.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a", size = 113005, upload-time = "2026-03-05T15:54:29.767Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a9/15d6b6f913294ea41b44d901741298e3718e1cb89ee626b3694625826a43/mmh3-5.2.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b", size = 120744, upload-time = "2026-03-05T15:54:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/70b73923fd0284c439860ff5c871b20210dfdbe9a6b9dd0ee6496d77f174/mmh3-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166", size = 99111, upload-time = "2026-03-05T15:54:32.353Z" }, + { url = "https://files.pythonhosted.org/packages/dd/38/99f7f75cd27d10d8b899a1caafb9d531f3903e4d54d572220e3d8ac35e89/mmh3-5.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16", size = 98623, upload-time = "2026-03-05T15:54:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/fd/68/6e292c0853e204c44d2f03ea5f090be3317a0e2d9417ecb62c9eb27687df/mmh3-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211", size = 106437, upload-time = "2026-03-05T15:54:35.177Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c6/fedd7284c459cfb58721d461fcf5607a4c1f5d9ab195d113d51d10164d16/mmh3-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000", size = 110002, upload-time = "2026-03-05T15:54:36.673Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ac/ca8e0c19a34f5b71390171d2ff0b9f7f187550d66801a731bb68925126a4/mmh3-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5", size = 97507, upload-time = "2026-03-05T15:54:37.804Z" }, + { url = "https://files.pythonhosted.org/packages/df/94/6ebb9094cfc7ac5e7950776b9d13a66bb4a34f83814f32ba2abc9494fc68/mmh3-5.2.1-cp312-cp312-win32.whl", hash = "sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025", size = 40773, upload-time = "2026-03-05T15:54:40.077Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/cd3527198cf159495966551c84a5f36805a10ac17b294f41f67b83f6a4d6/mmh3-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00", size = 41560, upload-time = "2026-03-05T15:54:41.148Z" }, + { url = "https://files.pythonhosted.org/packages/15/96/6fe5ebd0f970a076e3ed5512871ce7569447b962e96c125528a2f9724470/mmh3-5.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc", size = 39313, upload-time = "2026-03-05T15:54:42.171Z" }, + { url = "https://files.pythonhosted.org/packages/25/a5/9daa0508a1569a54130f6198d5462a92deda870043624aa3ea72721aa765/mmh3-5.2.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e", size = 40832, upload-time = "2026-03-05T15:54:43.212Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6b/3230c6d80c1f4b766dedf280a92c2241e99f87c1504ff74205ec8cebe451/mmh3-5.2.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d", size = 41964, upload-time = "2026-03-05T15:54:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/62/fb/648bfddb74a872004b6ee751551bfdda783fe6d70d2e9723bad84dbe5311/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6", size = 39114, upload-time = "2026-03-05T15:54:45.205Z" }, + { url = "https://files.pythonhosted.org/packages/95/c2/ab7901f87af438468b496728d11264cb397b3574d41506e71b92128e0373/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f", size = 39819, upload-time = "2026-03-05T15:54:46.509Z" }, + { url = "https://files.pythonhosted.org/packages/2f/ed/6f88dda0df67de1612f2e130ffea34cf84aaee5bff5b0aff4dbff2babe34/mmh3-5.2.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8", size = 40330, upload-time = "2026-03-05T15:54:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/3d/66/7516d23f53cdf90f43fce24ab80c28f45e6851d78b46bef8c02084edf583/mmh3-5.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6", size = 56078, upload-time = "2026-03-05T15:54:48.9Z" }, + { url = "https://files.pythonhosted.org/packages/bc/34/4d152fdf4a91a132cb226b671f11c6b796eada9ab78080fb5ce1e95adaab/mmh3-5.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:19bbd3b841174ae6ed588536ab5e1b1fe83d046e668602c20266547298d939a9", size = 40498, upload-time = "2026-03-05T15:54:49.942Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4c/8e3af1b6d85a299767ec97bd923f12b06267089c1472c27c1696870d1175/mmh3-5.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03", size = 40033, upload-time = "2026-03-05T15:54:50.994Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/966ea560e32578d453c9e9db53d602cbb1d0da27317e232afa7c38ceba11/mmh3-5.2.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fd96476f04db5ceba1cfa0f21228f67c1f7402296f0e73fee3513aa680ad237b", size = 97320, upload-time = "2026-03-05T15:54:52.072Z" }, + { url = "https://files.pythonhosted.org/packages/bb/0d/2c5f9893b38aeb6b034d1a44ecd55a010148054f6a516abe53b5e4057297/mmh3-5.2.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5", size = 103299, upload-time = "2026-03-05T15:54:53.569Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fc/2ebaef4a4d4376f89761274dc274035ffd96006ab496b4ee5af9b08f21a9/mmh3-5.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593", size = 106222, upload-time = "2026-03-05T15:54:55.092Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/ea7ffe126d0ba0406622602a2d05e1e1a6841cc92fc322eb576c95b27fad/mmh3-5.2.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4", size = 113048, upload-time = "2026-03-05T15:54:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/85/57/9447032edf93a64aa9bef4d9aa596400b1756f40411890f77a284f6293ca/mmh3-5.2.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1", size = 120742, upload-time = "2026-03-05T15:54:57.453Z" }, + { url = "https://files.pythonhosted.org/packages/53/82/a86cc87cc88c92e9e1a598fee509f0409435b57879a6129bf3b3e40513c7/mmh3-5.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104", size = 99132, upload-time = "2026-03-05T15:54:58.583Z" }, + { url = "https://files.pythonhosted.org/packages/54/f7/6b16eb1b40ee89bb740698735574536bc20d6cdafc65ae702ea235578e05/mmh3-5.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7e4e1f580033335c6f76d1e0d6b56baf009d1a64d6a4816347e4271ba951f46d", size = 98686, upload-time = "2026-03-05T15:55:00.078Z" }, + { url = "https://files.pythonhosted.org/packages/e8/88/a601e9f32ad1410f438a6d0544298ea621f989bd34a0731a7190f7dec799/mmh3-5.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f", size = 106479, upload-time = "2026-03-05T15:55:01.532Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/ce29ae3dfc4feec4007a437a1b7435fb9507532a25147602cd5b52be86db/mmh3-5.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2", size = 110030, upload-time = "2026-03-05T15:55:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/13/30/ae444ef2ff87c805d525da4fa63d27cda4fe8a48e77003a036b8461cfd5c/mmh3-5.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a", size = 97536, upload-time = "2026-03-05T15:55:04.135Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f9/dc3787ee5c813cc27fe79f45ad4500d9b5437f23a7402435cc34e07c7718/mmh3-5.2.1-cp313-cp313-win32.whl", hash = "sha256:54b64fb2433bc71488e7a449603bf8bd31fbcf9cb56fbe1eb6d459e90b86c37b", size = 40769, upload-time = "2026-03-05T15:55:05.277Z" }, + { url = "https://files.pythonhosted.org/packages/43/67/850e0b5a1e97799822ebfc4ca0e8c6ece3ed8baf7dcdf64de817dfdda2ca/mmh3-5.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:cae6383181f1e345317742d2ddd88f9e7d2682fa4c9432e3a74e47d92dce0229", size = 41563, upload-time = "2026-03-05T15:55:06.283Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/98c90b28e1da5458e19fbfaf4adb5289208d3bfccd45dd14eab216a2f0bb/mmh3-5.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:022aa1a528604e6c83d0a7705fdef0b5355d897a9e0fa3a8d26709ceaa06965d", size = 39310, upload-time = "2026-03-05T15:55:07.323Z" }, + { url = "https://files.pythonhosted.org/packages/63/b4/65bc1fb2bb7f83e91c30865023b1847cf89a5f237165575e8c83aa536584/mmh3-5.2.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:d771f085fcdf4035786adfb1d8db026df1eb4b41dac1c3d070d1e49512843227", size = 40794, upload-time = "2026-03-05T15:55:09.773Z" }, + { url = "https://files.pythonhosted.org/packages/c4/86/7168b3d83be8eb553897b1fac9da8bbb06568e5cfe555ffc329ebb46f59d/mmh3-5.2.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:7f196cd7910d71e9d9860da0ff7a77f64d22c1ad931f1dd18559a06e03109fc0", size = 41923, upload-time = "2026-03-05T15:55:10.924Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9b/b653ab611c9060ce8ff0ba25c0226757755725e789292f3ca138a58082cd/mmh3-5.2.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b1f12bd684887a0a5d55e6363ca87056f361e45451105012d329b86ec19dbe0b", size = 39131, upload-time = "2026-03-05T15:55:11.961Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b4/5a2e0d34ab4d33543f01121e832395ea510132ea8e52cdf63926d9d81754/mmh3-5.2.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d106493a60dcb4aef35a0fac85105e150a11cf8bc2b0d388f5a33272d756c966", size = 39825, upload-time = "2026-03-05T15:55:13.013Z" }, + { url = "https://files.pythonhosted.org/packages/bd/69/81699a8f39a3f8d368bec6443435c0c392df0d200ad915bf0d222b588e03/mmh3-5.2.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:44983e45310ee5b9f73397350251cdf6e63a466406a105f1d16cb5baa659270b", size = 40344, upload-time = "2026-03-05T15:55:14.026Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/71c8c775807606e8fd8acc5c69016e1caf3200d50b50b6dd4b40ce10b76c/mmh3-5.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:368625fb01666655985391dbad3860dc0ba7c0d6b9125819f3121ee7292b4ac8", size = 56291, upload-time = "2026-03-05T15:55:15.137Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/2c24517d4b2ce9e4917362d24f274d3d541346af764430249ddcc4cb3a08/mmh3-5.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:72d1cc63bcc91e14933f77d51b3df899d6a07d184ec515ea7f56bff659e124d7", size = 40575, upload-time = "2026-03-05T15:55:16.518Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b9/e4a360164365ac9f07a25f0f7928e3a66eb9ecc989384060747aa170e6aa/mmh3-5.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e8b4b5580280b9265af3e0409974fb79c64cf7523632d03fbf11df18f8b0181e", size = 40052, upload-time = "2026-03-05T15:55:17.735Z" }, + { url = "https://files.pythonhosted.org/packages/97/ca/120d92223a7546131bbbc31c9174168ee7a73b1366f5463ffe69d9e691fe/mmh3-5.2.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4cbbde66f1183db040daede83dd86c06d663c5bb2af6de1142b7c8c37923dd74", size = 97311, upload-time = "2026-03-05T15:55:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/b6/71/c1a60c1652b8813ef9de6d289784847355417ee0f2980bca002fe87f4ae5/mmh3-5.2.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8ff038d52ef6aa0f309feeba00c5095c9118d0abf787e8e8454d6048db2037fc", size = 103279, upload-time = "2026-03-05T15:55:20.448Z" }, + { url = "https://files.pythonhosted.org/packages/48/29/ad97f4be1509cdcb28ae32c15593ce7c415db47ace37f8fad35b493faa9a/mmh3-5.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4130d0b9ce5fad6af07421b1aecc7e079519f70d6c05729ab871794eded8617", size = 106290, upload-time = "2026-03-05T15:55:21.6Z" }, + { url = "https://files.pythonhosted.org/packages/77/29/1f86d22e281bd8827ba373600a4a8b0c0eae5ca6aa55b9a8c26d2a34decc/mmh3-5.2.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e0bfe77d238308839699944164b96a2eeccaf55f2af400f54dc20669d8d5f2", size = 113116, upload-time = "2026-03-05T15:55:22.826Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7c/339971ea7ed4c12d98f421f13db3ea576a9114082ccb59d2d1a0f00ccac1/mmh3-5.2.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f963eafc0a77a6c0562397da004f5876a9bcf7265a7bcc3205e29636bc4a1312", size = 120740, upload-time = "2026-03-05T15:55:24.3Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/3c7c4bdb8e926bb3c972d1e2907d77960c1c4b250b41e8366cf20c6e4373/mmh3-5.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:92883836caf50d5255be03d988d75bc93e3f86ba247b7ca137347c323f731deb", size = 99143, upload-time = "2026-03-05T15:55:25.456Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/33dd8706e732458c8375eae63c981292de07a406bad4ec03e5269654aa2c/mmh3-5.2.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:57b52603e89355ff318025dd55158f6e71396c0f1f609d548e9ea9c94cc6ce0a", size = 98703, upload-time = "2026-03-05T15:55:26.723Z" }, + { url = "https://files.pythonhosted.org/packages/51/04/76bbce05df76cbc3d396f13b2ea5b1578ef02b6a5187e132c6c33f99d596/mmh3-5.2.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f40a95186a72fa0b67d15fef0f157bfcda00b4f59c8a07cbe5530d41ac35d105", size = 106484, upload-time = "2026-03-05T15:55:28.214Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8f/c6e204a2c70b719c1f62ffd9da27aef2dddcba875ea9c31ca0e87b975a46/mmh3-5.2.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:58370d05d033ee97224c81263af123dea3d931025030fd34b61227a768a8858a", size = 110012, upload-time = "2026-03-05T15:55:29.532Z" }, + { url = "https://files.pythonhosted.org/packages/e3/37/7181efd8e39db386c1ebc3e6b7d1f702a09d7c1197a6f2742ed6b5c16597/mmh3-5.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7be6dfb49e48fd0a7d91ff758a2b51336f1cd21f9d44b20f6801f072bd080cdd", size = 97508, upload-time = "2026-03-05T15:55:31.01Z" }, + { url = "https://files.pythonhosted.org/packages/42/0f/afa7ca2615fd85e1469474bb860e381443d0b868c083b62b41cb1d7ca32f/mmh3-5.2.1-cp314-cp314-win32.whl", hash = "sha256:54fe8518abe06a4c3852754bfd498b30cc58e667f376c513eac89a244ce781a4", size = 41387, upload-time = "2026-03-05T15:55:32.403Z" }, + { url = "https://files.pythonhosted.org/packages/71/0d/46d42a260ee1357db3d486e6c7a692e303c017968e14865e00efa10d09fc/mmh3-5.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:3f796b535008708846044c43302719c6956f39ca2d93f2edda5319e79a29efbb", size = 42101, upload-time = "2026-03-05T15:55:33.646Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7b/848a8378059d96501a41159fca90d6a99e89736b0afbe8e8edffeac8c74b/mmh3-5.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:cd471ede0d802dd936b6fab28188302b2d497f68436025857ca72cd3810423fe", size = 39836, upload-time = "2026-03-05T15:55:35.026Z" }, + { url = "https://files.pythonhosted.org/packages/27/61/1dabea76c011ba8547c25d30c91c0ec22544487a8750997a27a0c9e1180b/mmh3-5.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:5174a697ce042fa77c407e05efe41e03aa56dae9ec67388055820fb48cf4c3ba", size = 57727, upload-time = "2026-03-05T15:55:36.162Z" }, + { url = "https://files.pythonhosted.org/packages/b7/32/731185950d1cf2d5e28979cc8593016ba1619a295faba10dda664a4931b5/mmh3-5.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0a3984146e414684a6be2862d84fcb1035f4984851cb81b26d933bab6119bf00", size = 41308, upload-time = "2026-03-05T15:55:37.254Z" }, + { url = "https://files.pythonhosted.org/packages/76/aa/66c76801c24b8c9418b4edde9b5e57c75e72c94e29c48f707e3962534f18/mmh3-5.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bd6e7d363aa93bd3421b30b6af97064daf47bc96005bddba67c5ffbc6df426b8", size = 40758, upload-time = "2026-03-05T15:55:38.61Z" }, + { url = "https://files.pythonhosted.org/packages/9e/bb/79a1f638a02f0ae389f706d13891e2fbf7d8c0a22ecde67ba828951bb60a/mmh3-5.2.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:113f78e7463a36dbbcea05bfe688efd7fa759d0f0c56e73c974d60dcfec3dfcc", size = 109670, upload-time = "2026-03-05T15:55:40.13Z" }, + { url = "https://files.pythonhosted.org/packages/26/94/8cd0e187a288985bcfc79bf5144d1d712df9dee74365f59d26e3a1865be6/mmh3-5.2.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e8ec5f606e0809426d2440e0683509fb605a8820a21ebd120dcdba61b74ef7f", size = 117399, upload-time = "2026-03-05T15:55:42.076Z" }, + { url = "https://files.pythonhosted.org/packages/42/94/dfea6059bd5c5beda565f58a4096e43f4858fb6d2862806b8bbd12cbb284/mmh3-5.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22b0f9971ec4e07e8223f2beebe96a6cfc779d940b6f27d26604040dd74d3a44", size = 120386, upload-time = "2026-03-05T15:55:43.481Z" }, + { url = "https://files.pythonhosted.org/packages/47/cb/f9c45e62aaa67220179f487772461d891bb582bb2f9783c944832c60efd9/mmh3-5.2.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85ffc9920ffc39c5eee1e3ac9100c913a0973996fbad5111f939bbda49204bb7", size = 125924, upload-time = "2026-03-05T15:55:44.638Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/fe54a4a7c11bc9f623dfc1707decd034245602b076dfc1dcc771a4163170/mmh3-5.2.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7aec798c2b01aaa65a55f1124f3405804184373abb318a3091325aece235f67c", size = 135280, upload-time = "2026-03-05T15:55:45.866Z" }, + { url = "https://files.pythonhosted.org/packages/97/67/fe7e9e9c143daddd210cd22aef89cbc425d58ecf238d2b7d9eb0da974105/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55dbbd8ffbc40d1697d5e2d0375b08599dae8746b0b08dea05eee4ce81648fac", size = 110050, upload-time = "2026-03-05T15:55:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/43/c4/6d4b09fcbef80794de447c9378e39eefc047156b290fa3dd2d5257ca8227/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6c85c38a279ca9295a69b9b088a2e48aa49737bb1b34e6a9dc6297c110e8d912", size = 111158, upload-time = "2026-03-05T15:55:48.239Z" }, + { url = "https://files.pythonhosted.org/packages/81/a6/ca51c864bdb30524beb055a6d8826db3906af0834ec8c41d097a6e8573d5/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6290289fa5fb4c70fd7f72016e03633d60388185483ff3b162912c81205ae2cf", size = 116890, upload-time = "2026-03-05T15:55:49.405Z" }, + { url = "https://files.pythonhosted.org/packages/cc/04/5a1fe2e2ad843d03e89af25238cbc4f6840a8bb6c4329a98ab694c71deda/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:4fc6cd65dc4d2fdb2625e288939a3566e36127a84811a4913f02f3d5931da52d", size = 123121, upload-time = "2026-03-05T15:55:50.61Z" }, + { url = "https://files.pythonhosted.org/packages/af/4d/3c820c6f4897afd25905270a9f2330a23f77a207ea7356f7aadace7273c0/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:623f938f6a039536cc02b7582a07a080f13fdfd48f87e63201d92d7e34d09a18", size = 110187, upload-time = "2026-03-05T15:55:52.143Z" }, + { url = "https://files.pythonhosted.org/packages/21/54/1d71cd143752361c0aebef16ad3f55926a6faf7b112d355745c1f8a25f7f/mmh3-5.2.1-cp314-cp314t-win32.whl", hash = "sha256:29bc3973676ae334412efdd367fcd11d036b7be3efc1ce2407ef8676dabfeb82", size = 41934, upload-time = "2026-03-05T15:55:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e4/63a2a88f31d93dea03947cccc2a076946857e799ea4f7acdecbf43b324aa/mmh3-5.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:28cfab66577000b9505a0d068c731aee7ca85cd26d4d63881fab17857e0fe1fb", size = 43036, upload-time = "2026-03-05T15:55:55.252Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0f/59204bf136d1201f8d7884cfbaf7498c5b4674e87a4c693f9bde63741ce1/mmh3-5.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dfd51b4c56b673dfbc43d7d27ef857dd91124801e2806c69bb45585ce0fa019b", size = 40391, upload-time = "2026-03-05T15:55:56.697Z" }, +] + [[package]] name = "mplsoccer" version = "1.6.0" @@ -3623,6 +4013,152 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/54/db/160dffb57ed9a3705c4cbcbff0ac03bdae45f1ca7d58ab74645550df3fbd/pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf", size = 2107999, upload-time = "2025-11-04T13:42:03.885Z" }, + { url = "https://files.pythonhosted.org/packages/a3/7d/88e7de946f60d9263cc84819f32513520b85c0f8322f9b8f6e4afc938383/pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5", size = 1929745, upload-time = "2025-11-04T13:42:06.075Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c2/aef51e5b283780e85e99ff19db0f05842d2d4a8a8cd15e63b0280029b08f/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d", size = 1920220, upload-time = "2025-11-04T13:42:08.457Z" }, + { url = "https://files.pythonhosted.org/packages/c7/97/492ab10f9ac8695cd76b2fdb24e9e61f394051df71594e9bcc891c9f586e/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60", size = 2067296, upload-time = "2025-11-04T13:42:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/ec/23/984149650e5269c59a2a4c41d234a9570adc68ab29981825cfaf4cfad8f4/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82", size = 2231548, upload-time = "2025-11-04T13:42:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/71/0c/85bcbb885b9732c28bec67a222dbed5ed2d77baee1f8bba2002e8cd00c5c/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5", size = 2362571, upload-time = "2025-11-04T13:42:16.208Z" }, + { url = "https://files.pythonhosted.org/packages/c0/4a/412d2048be12c334003e9b823a3fa3d038e46cc2d64dd8aab50b31b65499/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3", size = 2068175, upload-time = "2025-11-04T13:42:18.911Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/c58b6a776b502d0a5540ad02e232514285513572060f0d78f7832ca3c98b/pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425", size = 2177203, upload-time = "2025-11-04T13:42:22.578Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ae/f06ea4c7e7a9eead3d165e7623cd2ea0cb788e277e4f935af63fc98fa4e6/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504", size = 2148191, upload-time = "2025-11-04T13:42:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/c1/57/25a11dcdc656bf5f8b05902c3c2934ac3ea296257cc4a3f79a6319e61856/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5", size = 2343907, upload-time = "2025-11-04T13:42:27.683Z" }, + { url = "https://files.pythonhosted.org/packages/96/82/e33d5f4933d7a03327c0c43c65d575e5919d4974ffc026bc917a5f7b9f61/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3", size = 2322174, upload-time = "2025-11-04T13:42:30.776Z" }, + { url = "https://files.pythonhosted.org/packages/81/45/4091be67ce9f469e81656f880f3506f6a5624121ec5eb3eab37d7581897d/pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460", size = 1990353, upload-time = "2025-11-04T13:42:33.111Z" }, + { url = "https://files.pythonhosted.org/packages/44/8a/a98aede18db6e9cd5d66bcacd8a409fcf8134204cdede2e7de35c5a2c5ef/pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b", size = 2015698, upload-time = "2025-11-04T13:42:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -3632,6 +4168,106 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pyiceberg" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "cachetools", marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "fsspec", version = "2025.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "mmh3", version = "5.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pydantic", marker = "python_full_version < '3.10'" }, + { name = "pyparsing", marker = "python_full_version < '3.10'" }, + { name = "pyroaring", marker = "python_full_version < '3.10'" }, + { name = "requests", marker = "python_full_version < '3.10'" }, + { name = "rich", marker = "python_full_version < '3.10'" }, + { name = "sortedcontainers", marker = "python_full_version < '3.10'" }, + { name = "strictyaml", marker = "python_full_version < '3.10'" }, + { name = "tenacity", version = "9.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/0e/90e61c38504f4fbd5ed79631f85da7d5ea5e5bf997bdeaa65b28ebf04cab/pyiceberg-0.10.0.tar.gz", hash = "sha256:2525afa5e7e5fc4e72b291f8e1cc219e982d2bda5ff17e62cd05b8d91c4139f5", size = 842633, upload-time = "2025-09-11T14:59:34.044Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/82/5dbcf36c13ddf528a6c4ba7f75ace2766859870e7e166096c73c8e63c457/pyiceberg-0.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:03a4f208f0c59c040d2a6ff51b952479358810aac28c5271de3fd1fa425f063c", size = 553282, upload-time = "2025-09-11T14:58:58.667Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a0/ca556da105ce64269e46977204777f6d5e1d8595f711f6d0edb3bbc58eff/pyiceberg-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6924f496f9a6e36f6530ce66483486f71d3cb4c08512d5aeb21095a9aa22d4b7", size = 550649, upload-time = "2025-09-11T14:59:00.931Z" }, + { url = "https://files.pythonhosted.org/packages/fa/6c/def488007282c3d9ece0ca68fd6c4c7f42c500c0125cba3b8767731bfe5e/pyiceberg-0.10.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e549ca852233b1aa20f2af1a8f9276b4a064c2515be0d73d36f28282502b8728", size = 906149, upload-time = "2025-09-11T14:59:03.47Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a3/08431f7c48ce3f236b1705586c73797eb03b86ca848b12adbbc2b60ed65b/pyiceberg-0.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:14a33da7eb579a02fc559d2e7b703a8133e81bc206bf11cb76ece9333ddfb7f3", size = 903762, upload-time = "2025-09-11T14:59:05.416Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bf/5c1c978dad3e4c52c48ab40749f1e49ee0dba26872863a0a3b4385cf81d3/pyiceberg-0.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:4dfafd712fec5c3776fafbb444784e20256b8674cbe243487f8cd7f99a6e8836", size = 549059, upload-time = "2025-09-11T14:59:06.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/62/b6f7bed760d0896958d046ca3c188fd15467c6502bcc2dc301ac0554c1ce/pyiceberg-0.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c799c9149e06ef9ece22945d5c198ffc69f5c04b314b59a43c2d4c1bb9ade84", size = 591127, upload-time = "2025-09-11T14:59:08.72Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b2/294c74e70c68744a8246924fee350095cc46f97f81d1e37125011d8e1bcb/pyiceberg-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a8c7070fe1262f50694b12241b5373ee89c8aededda82ef325cb14e5a95cc461", size = 587041, upload-time = "2025-09-11T14:59:10.643Z" }, + { url = "https://files.pythonhosted.org/packages/7a/2f/9a9f0a01f0dae2cefc024a2bd84a00ff2a5d8d952f37053c46523c1dd7a6/pyiceberg-0.10.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0d1a4896f546b1e115ece4212dd02b383eeb3c7ff5c072624b15f531b776f36", size = 1135929, upload-time = "2025-09-11T14:59:12.164Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c2/51deddeec916d44a04cc26053179b560ffceba72e4561b6cf58a64aea209/pyiceberg-0.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1b0ef2f1880dd7549cc54ccb1a25f61ad5329e079cba372b4c239b0012aecac6", size = 1131851, upload-time = "2025-09-11T14:59:13.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cc/e9cf3fa56d67306ba29352d56152907a91ca29eabc1a30d3177cee0d1418/pyiceberg-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:2127c795e451b971bd3f55cbda2d2c8200182bec3476e590e4a3453e60efda3c", size = 583472, upload-time = "2025-09-11T14:59:15.173Z" }, + { url = "https://files.pythonhosted.org/packages/03/61/f5042dd09cb91deed908a39acd5012f1ac6910ddf84ada889751732f0df8/pyiceberg-0.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64cad9d1db08192605875a872152cbcaca147ea486cfa94773fa5f4f65d78a23", size = 629281, upload-time = "2025-09-11T14:59:17.585Z" }, + { url = "https://files.pythonhosted.org/packages/8e/50/960f7239eedd4b1bab2a611f5e100fffc138549c1213760a57cd24a5bac1/pyiceberg-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3e12cf585318f0f48d31a77b4149e0e5b4c41e03a24aa8612e060f20ff41eb10", size = 623424, upload-time = "2025-09-11T14:59:19.045Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2b/756a74c80db6edd82c8d3f23c3ae13e7d6620300b87ef792c2a4d3935b30/pyiceberg-0.10.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6979dd741cee263c1235595f71888c73365f2725697411027c4bd81046db3294", size = 1377048, upload-time = "2025-09-11T14:59:20.541Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/9c18cb4ddc7d371db63714abb2f5e8414bc7a4d63f474644a2aea2933fe6/pyiceberg-0.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:13fd03ec3da6eb4d3b55ff94b647946a7749bede5d743c75b39deaad26421200", size = 1369921, upload-time = "2025-09-11T14:59:22.134Z" }, + { url = "https://files.pythonhosted.org/packages/7b/b3/c012dc6b5bc3d0a84821936789c753f5c44aec619b64fbcf7f90038d172e/pyiceberg-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:33367c84bcb0a2fbbe54cbbfe062691ab93b91a2e3d319bb546ec5b9b45b6057", size = 617722, upload-time = "2025-09-11T14:59:23.67Z" }, + { url = "https://files.pythonhosted.org/packages/e7/65/8a93fe2d72a99700da6372d68c78c551e1fdb8ce441b570d724506faaf93/pyiceberg-0.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:14cb3a5186e64f2ab37bc69cd7d1b32b25f416c87f9dadbcaa4f8e21b6c4e7b1", size = 516206, upload-time = "2025-09-11T14:59:25.224Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e3/554a2130a6e137a6621fd6c064937354a8bf3406cf3f405a23a5a356f4e2/pyiceberg-0.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9631d892f0977fbaef0498f088cb8535cd6b933606946dcce214a9a342d9c009", size = 514863, upload-time = "2025-09-11T14:59:27.522Z" }, + { url = "https://files.pythonhosted.org/packages/a9/87/bdf9f0751e7501ac247fe8a49a65d68bf84e671242a1c6ec15a3337bcfc3/pyiceberg-0.10.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:79016a97ec70a11e2920791fe2620b001bc5b657380d3d3ddf9f6a48af209615", size = 692516, upload-time = "2025-09-11T14:59:29.123Z" }, + { url = "https://files.pythonhosted.org/packages/34/de/c6a601fc18bce3d570393be3842dd2174a8f0e8facb4bee93e553e2e502e/pyiceberg-0.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:912128d6b70313002b5418096c444afcde3d541e53b5e6a6b4df177531ac5686", size = 691178, upload-time = "2025-09-11T14:59:30.459Z" }, + { url = "https://files.pythonhosted.org/packages/31/df/10b7d7da19efbbbbbf2f5452d8529b83853b0b82b871303702e693ef6994/pyiceberg-0.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:0d76efec0409536bf48146961abc2b94e4001c647d348da8ea0f8ccca6504d1f", size = 515431, upload-time = "2025-09-11T14:59:31.742Z" }, +] + +[[package]] +name = "pyiceberg" +version = "0.11.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "cachetools", marker = "python_full_version >= '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "fsspec", version = "2026.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mmh3", version = "5.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "pyparsing", marker = "python_full_version >= '3.10'" }, + { name = "pyroaring", marker = "python_full_version >= '3.10'" }, + { name = "requests", marker = "python_full_version >= '3.10'" }, + { name = "rich", marker = "python_full_version >= '3.10'" }, + { name = "strictyaml", marker = "python_full_version >= '3.10'" }, + { name = "tenacity", version = "9.1.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "zstandard", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/f0/7616676603fdbd05ab97816337a9b31be08a5f9e1ffd636260812b217e0f/pyiceberg-0.11.1.tar.gz", hash = "sha256:366fe0d5a74e3cf1d4e7cbf3c49e308da60e7835ea268667be9185388f05d7a5", size = 1076075, upload-time = "2026-03-03T00:10:27.61Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/40/ea9cc312ea8ad76d94c57f921be19256c3d07354056c68e64601ff43f98d/pyiceberg-0.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:89c347170a691e3b8d072519f36790b9c19a2263ec0af0144c873994ecfe55eb", size = 532405, upload-time = "2026-03-03T00:09:49.364Z" }, + { url = "https://files.pythonhosted.org/packages/97/e9/fbcd52d19aa2a362ba7fd971b1721c57f8c0fd37b5bb418ac283dde9637e/pyiceberg-0.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:138983e96a5c473aae1e1cd6143aacf02ccc9bbbf6180f15fa2588f99531188b", size = 533282, upload-time = "2026-03-03T00:09:50.712Z" }, + { url = "https://files.pythonhosted.org/packages/1d/43/a0722863f821e96eed8ff5753a7146dd5b1537668d6684ac650a48647669/pyiceberg-0.11.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e08c268e12d54ea629a1f123bf7cb9587bdd57fd7bcdac22394dc6272a18668f", size = 708618, upload-time = "2026-03-03T00:09:52.427Z" }, + { url = "https://files.pythonhosted.org/packages/72/76/3f4aa7f8fe99292b8074720f5726040f6c7528209666444f1d6166c17aeb/pyiceberg-0.11.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23ed91f8d2fa75a109708338f80a7addd2f37c93a8b0e6c9d220f1427939b81f", size = 705581, upload-time = "2026-03-03T00:09:54.389Z" }, + { url = "https://files.pythonhosted.org/packages/27/12/85543a838b2b3f4a74dfd8de470f6d88f6ba5d287231b6ea4e8765df04f6/pyiceberg-0.11.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4b91f1b1bac135c077326d30a1876f917e6c36b844a2e329b148fb9cb7aa660", size = 704197, upload-time = "2026-03-03T00:09:55.809Z" }, + { url = "https://files.pythonhosted.org/packages/f6/20/7a37d331722a4cfda0e782ec583aa28af412f8564ac937733eac440aa7b0/pyiceberg-0.11.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7770e7bdee34549e8baf28b8626e6552de7894d869465a7c95fd9af33b4cbeb0", size = 703982, upload-time = "2026-03-03T00:09:57.098Z" }, + { url = "https://files.pythonhosted.org/packages/65/16/61c2af5ab30f61f3b523346d109bc52b035bb26ecdfa58524c63fac190c4/pyiceberg-0.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:ccac408873f65b8954858ef08d4b098841301e232afc1b14e3eada489d303a7f", size = 530720, upload-time = "2026-03-03T00:09:58.548Z" }, + { url = "https://files.pythonhosted.org/packages/62/f7/3b7fee2ecc021f0526f23ef4ae5dcc8e0ed26062c35890ad25d39c53fb3b/pyiceberg-0.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba98d6a41ec0b7c81dd85d764f15653d6abbbbd69d92630677c43f92dd50d924", size = 532406, upload-time = "2026-03-03T00:09:59.647Z" }, + { url = "https://files.pythonhosted.org/packages/94/25/324030b13d91b7b564fb7342bf3fcdbf76eed2672964b273b156bf84d6e5/pyiceberg-0.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6400774e6820760eb6c322f6feace43fe7267deb9f8d508f10bf258887a9c4d5", size = 533368, upload-time = "2026-03-03T00:10:01.264Z" }, + { url = "https://files.pythonhosted.org/packages/1c/83/6a43d06a079292c4fc7815b4de3e90a05ded90031c35d0a1b037659f722b/pyiceberg-0.11.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1663d79fc8400903992c63f79b3908b9298c623138e8929bf36c559231e082d3", size = 722886, upload-time = "2026-03-03T00:10:02.558Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5c/53807036b63bb810f2c56b4b5576e79b721ba93f1c16bd0dd49ecfe41055/pyiceberg-0.11.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:856c7fca5ed780ed44f60bceb92d6b311ebc008a2249415b8f6045201d4f5530", size = 721212, upload-time = "2026-03-03T00:10:03.694Z" }, + { url = "https://files.pythonhosted.org/packages/db/11/65e25d6016e3844c516c9f04041853115711f64af1ee184a2320c9ceab4c/pyiceberg-0.11.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf94756fb6a822d20a5a64f44840e6633ebf8b1deb3ce01057bff1cc03b01c2", size = 717978, upload-time = "2026-03-03T00:10:05.011Z" }, + { url = "https://files.pythonhosted.org/packages/52/0b/33b4ea9dc7f0c496900f5fc6da79e8587e94b88e2244ff02b786016cc649/pyiceberg-0.11.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8bd52c1891ae74cee21a4ebe8325953310a2e0af5352d70f47f5461422fcce2d", size = 718747, upload-time = "2026-03-03T00:10:06.269Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b6/5e133c435efc577afea5be303d7123264a5176a3ce1e2d3dc3a691049eaf/pyiceberg-0.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:65a7ad892a570045b0de2db6af17119162880aebc05a0c125ce2db7dab36f17e", size = 531081, upload-time = "2026-03-03T00:10:07.352Z" }, + { url = "https://files.pythonhosted.org/packages/8f/84/a140466b7e0841207e6b77042e03d4ab3a4f9d47e00f0bbbcc5420792bbb/pyiceberg-0.11.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd423b8ee2f75fc9db09158875abe5e2c952a26ae5e521c3265ab2f9d3511ddf", size = 532981, upload-time = "2026-03-03T00:10:08.906Z" }, + { url = "https://files.pythonhosted.org/packages/17/10/6bedd784010f707680ffd0606d4d11394cf915f4f9f54ae16e8007e00ad4/pyiceberg-0.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e273242cdca56029af694d7ce18075d47a74d034326d663ff6dd2655a6f44825", size = 533188, upload-time = "2026-03-03T00:10:10.086Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a3/79db617c3cffc963efa8a332707079d3f22fd58067b31a208d358dd89b39/pyiceberg-0.11.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b347d3cc8510f8fbe191956fcda7da372ebb3302789acefca08e352345959003", size = 729546, upload-time = "2026-03-03T00:10:11.413Z" }, + { url = "https://files.pythonhosted.org/packages/06/64/acc11d230c33817bced80d9d947bb49e7bb3a429d76d906523e3df86faf8/pyiceberg-0.11.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba3a35b4648694783aeae5b77c235a57191c8b1b375c8602b03ae56a6cf4fe7", size = 730263, upload-time = "2026-03-03T00:10:13.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1a/fb067d5150c7309fbf5dd126c648a6afed6259e7bc924ba3c65d0f87a333/pyiceberg-0.11.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0f958cbca18d05846e3081dfff8575e73d45595441d659847479656dc76f91d", size = 724064, upload-time = "2026-03-03T00:10:14.55Z" }, + { url = "https://files.pythonhosted.org/packages/c1/71/103fdba5b144d55f3bb07347893737cc1d8fd71308108a77b7817c92c544/pyiceberg-0.11.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8c62636a1e9d8a1fc74ffb70383939b9cd93f2c9ee8e12015a50dd75c98a989e", size = 727239, upload-time = "2026-03-03T00:10:16.204Z" }, + { url = "https://files.pythonhosted.org/packages/18/c3/4db64429304c58c039f8e842cd37a9a1c472f596c2868ed2a5d2907b17ed/pyiceberg-0.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:1d6b6f0c1e7dd8357f1ba56524bfc870d04ad3c00979db291784a7145497ad3b", size = 531309, upload-time = "2026-03-03T00:10:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/4c/a122d80d98cb6125d87024681263406433f0c25c699d503f5633521e6809/pyiceberg-0.11.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b7ec5db19feab98a31fcd5caccf4a9a4e83f96933d1ca393ba7aea665710c2bb", size = 532644, upload-time = "2026-03-03T00:10:18.574Z" }, + { url = "https://files.pythonhosted.org/packages/10/94/9a8fa5fc580e6dccd34bbbf51e7658cd7b49540e2458783addeff5e22a91/pyiceberg-0.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cec0616d2ba6e7dda6327089a2f34ec723aa9ac2c389857ef0b83f65fb135dd6", size = 532787, upload-time = "2026-03-03T00:10:19.656Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ab/ab7c88828bc17d77dbbc5a765419dfec2135629e1d74cdd0762cd38ad867/pyiceberg-0.11.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddb360da76c62c7c23ec3da40e1af48e6712a563905fea2d1a8911ff7a3b6c4d", size = 722202, upload-time = "2026-03-03T00:10:21.012Z" }, + { url = "https://files.pythonhosted.org/packages/df/38/079cf1c0bf86da315472a926eec0dba10135f43374a2e267336eb98d8c76/pyiceberg-0.11.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d8790f420ebc484236017edba59182cf2a21bd3e4224a0bd0760a9c7268e96a", size = 724037, upload-time = "2026-03-03T00:10:22.176Z" }, + { url = "https://files.pythonhosted.org/packages/08/6b/08eaef477debb110438d943ef3f5985096f660ccb735d6344701cbd075a9/pyiceberg-0.11.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ae27ba4d37925d5b2cff192acaa70c8bb114d632bbc527cc91fea0370702b866", size = 716035, upload-time = "2026-03-03T00:10:23.789Z" }, + { url = "https://files.pythonhosted.org/packages/0b/59/7671d6a630ab1d85c6e7ca8ddf438dc63a0b0dd183bc4be69bf25c0fa5f6/pyiceberg-0.11.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db66a4e0fdfbf4090631d59c3f65e960d9a5561e9259f6f3993cbe91e396837e", size = 720887, upload-time = "2026-03-03T00:10:24.824Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2b/5c8ad37807efaedb14b20f01f36462684468c80da5b74f4018fb4c1804b5/pyiceberg-0.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:eb3a0a3e630ee89758eb96b39b456f4697732351fb0c080e9498ea578f9b71f9", size = 530923, upload-time = "2026-03-03T00:10:26.196Z" }, +] + [[package]] name = "pyparsing" version = "3.3.2" @@ -3641,6 +4277,98 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] +[[package]] +name = "pyroaring" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/71/134bcaf93d8734051ecbc164dabe695645849249e0fb24209b2dd88e8147/pyroaring-1.0.4.tar.gz", hash = "sha256:99d4217bdfeedc91b82efcec940175a9f9a9137c6476faf7ce5d9c9dd889c8e6", size = 189155, upload-time = "2026-03-19T13:57:27.932Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/b3/5a910bcad7295a7198619540a4d879e447e5e0bf391a18f26e4f6bbd86d0/pyroaring-1.0.4-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:1c5f78aeccf2ce4580cd373f50c20b3e6279a594d6fb827266dbceede9dd8c92", size = 320196, upload-time = "2026-03-19T13:55:31.771Z" }, + { url = "https://files.pythonhosted.org/packages/1f/66/e9d05f2e6d496d60cc7264a64d73d342bd099f164a7cdf2d57386bed4cce/pyroaring-1.0.4-cp310-cp310-macosx_14_0_universal2.whl", hash = "sha256:c0e7768e2143b842f5833857bbcd0ba28b0022028654ca5be395b7320b548460", size = 681634, upload-time = "2026-03-19T13:55:33.049Z" }, + { url = "https://files.pythonhosted.org/packages/6d/52/4e3f4dfd44edb425e43d4a4400308c564750b7f538fb625ca74c05e334fd/pyroaring-1.0.4-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:7497ea5124523bae7c05726e38faa9fe2f6e64c2423f96f0e4a00078d8c1b9db", size = 360359, upload-time = "2026-03-19T13:55:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1e/31e5ea70966af4af507efd1c44240df5f09db73b6a04bdcb582377b9d870/pyroaring-1.0.4-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:827e7ecfa57a0f6e1cfd2449eabbc04fabcc16e4b3ad0f59f7a2d08f5d2fd063", size = 1885434, upload-time = "2026-03-19T13:55:35.668Z" }, + { url = "https://files.pythonhosted.org/packages/81/e1/98bc17b321e6c838edb64c166e8661341b05539bde815e0198a34f56ccba/pyroaring-1.0.4-cp310-cp310-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d3125ddc6e9e9dd58663db2db67c5ef12ac76d7a7e1882cf82581cd2307ac82b", size = 1751735, upload-time = "2026-03-19T13:55:37.194Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4e/a7d94c948450ab8f63a400d1f5cad61c2135c2b52d060865075027b296db/pyroaring-1.0.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6633a7a91df7aea8340e348408b4eb92a2cd89eae0f99e9b42b1d6eb6a1c9673", size = 2078662, upload-time = "2026-03-19T13:55:38.553Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e9/e3da53c33de08e5be4e6fd5f6444d490bb631d0b0c13bc5f753eeab5c376/pyroaring-1.0.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fcee7f9cd9dcce96e057ae78b7deef0eced728b7452dcf2781b366bd69db3432", size = 2809536, upload-time = "2026-03-19T13:55:40.135Z" }, + { url = "https://files.pythonhosted.org/packages/6c/30/2249294290890dfdbb9d8537e5b322cd3386f716f62dafcccde1aa33edd3/pyroaring-1.0.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:dfabce5b120dd50dc46d11066ec111732d19a4f99e5a7720271dbab5b13bb692", size = 2610604, upload-time = "2026-03-19T13:55:41.485Z" }, + { url = "https://files.pythonhosted.org/packages/8b/71/368c5b35af66fea64bf4fcb461d55857de4ff484aa3f4ff55ce2f1c2e24e/pyroaring-1.0.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a92af0bf8ad040b513ef8867fda2669708a1b81325295c55cb4e8cc3a835321a", size = 3052109, upload-time = "2026-03-19T13:55:43.193Z" }, + { url = "https://files.pythonhosted.org/packages/7c/79/556ac6b249e2c73dc2fb224baab9ecb497217d711b848c8a9a42a5b5c921/pyroaring-1.0.4-cp310-cp310-win32.whl", hash = "sha256:49a354fdd9359a11c2da5a70d5c202ee339a4328d2c57679acf642e577202b87", size = 202780, upload-time = "2026-03-19T13:55:44.817Z" }, + { url = "https://files.pythonhosted.org/packages/31/ce/d68e2485e3b87e650535be76ed6344b663f41ad723d11a60405e375c8d7a/pyroaring-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:387001bcaa1653c6770eb8dd88f6427cbcef882c2ab8a812949dd8d98d09d545", size = 252209, upload-time = "2026-03-19T13:55:46.087Z" }, + { url = "https://files.pythonhosted.org/packages/14/65/1c29ad4ac8986c0f89ed76ff3ca5c83ca8aeed268594796dec25554bb3f5/pyroaring-1.0.4-cp310-cp310-win_arm64.whl", hash = "sha256:a7553a23a806033ee93f5bd50f7c963d713a26ebecd55f173d24404a8673317d", size = 216508, upload-time = "2026-03-19T13:55:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/84/1c/f96ef38332d29015ef5285c0f6117b5644ccd9930334038740aed1df0a09/pyroaring-1.0.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:8e1b2a9f6f6c08e971098867186bfd23fd08afd899db318cc9ed602913b1fc54", size = 319867, upload-time = "2026-03-19T13:55:48.492Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a5/0759a5b55fe4ea0ef322a134117dc2a706ac76e6dcd1706b1de4c5af66a3/pyroaring-1.0.4-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:8a202294c7bfd19c2d08e95e03e486859b351a6f1daf0d69197bd8d355d4044a", size = 680818, upload-time = "2026-03-19T13:55:49.696Z" }, + { url = "https://files.pythonhosted.org/packages/7f/47/f681a3fe58f1ca2712c33b1003e40b3716d0eab267abf8e6ae57b19c9245/pyroaring-1.0.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:f8157345c01f633f759fc47133a076d3307f85d0246dd17986127fd1e2c349d0", size = 359867, upload-time = "2026-03-19T13:55:51.053Z" }, + { url = "https://files.pythonhosted.org/packages/58/6f/bdcf4b8ba51f921bc5184feb07985dcb32b0aacb78d7003d153e0883afb1/pyroaring-1.0.4-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:969c24580c9c16daa8e36eb76729d8c59c76250890dc6acdae9adb4d41107c4b", size = 1951858, upload-time = "2026-03-19T13:55:52.413Z" }, + { url = "https://files.pythonhosted.org/packages/ac/40/3475ba1a455df6ff617e9babbe998b8973483266384cd5ff4072914228da/pyroaring-1.0.4-cp311-cp311-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2984402d7c003923dc2e9b3e05676417f1de66ec31e4988d4b54c0a6008fe269", size = 1797084, upload-time = "2026-03-19T13:55:53.685Z" }, + { url = "https://files.pythonhosted.org/packages/50/5a/e4426481a29099095dd56b1685a78039456f36cc4afedd55a09b9c137ac3/pyroaring-1.0.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d85f85aa2f56a5dde9c8d97cbfaf80dfe9c7212171f7f263043d0800b44cee3a", size = 2145587, upload-time = "2026-03-19T13:55:55.026Z" }, + { url = "https://files.pythonhosted.org/packages/b8/05/8d14ebe27b888433b61f5a9af8e99b368e58370102449299fecea4501d3a/pyroaring-1.0.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0b69e8799062acf859b13f5369c0976a6033211e72c51f8b7bc817925cab193f", size = 2875499, upload-time = "2026-03-19T13:55:56.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/9a/4fd95a78f6de7dd5c39db8c34905e9fc7a538218abcec76a141d425779e7/pyroaring-1.0.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0333b18734475c531977e7a9027fc5753d7cac836f2475198f630ecefbbfd764", size = 2667086, upload-time = "2026-03-19T13:55:58.431Z" }, + { url = "https://files.pythonhosted.org/packages/43/ff/53af720a9b65858344dcd0765f57df9900ce5c1020038d33d2bbff78203f/pyroaring-1.0.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0736ee92f8781570d5be22a4f43238937676518a18487b1efafca67a63a89099", size = 3116570, upload-time = "2026-03-19T13:55:59.683Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b7/f278f468acea40f8d3989c803cbb40ee222a86892abdacfe1e882788e292/pyroaring-1.0.4-cp311-cp311-win32.whl", hash = "sha256:76bdac16f535154322ca734cf51d96fa8f8d5a2f8d009a24affba0036f74f677", size = 201943, upload-time = "2026-03-19T13:56:00.905Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4567fa392b4cc5407471b0c1aa8263674dd0611cd51012daf58ada694d4e/pyroaring-1.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:910c177c3ff77db0d306119963583be85a98870d396472b5a0153babc8b7fcf7", size = 253022, upload-time = "2026-03-19T13:56:02.124Z" }, + { url = "https://files.pythonhosted.org/packages/17/5b/1676f27f95ebadfa3a221c146ee941db0e91dcd08d1ee21c45106f9e329a/pyroaring-1.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:c2e909a436ef22b9ba9f7dbc0e88fb684ced3bc6d5192b8315fe5ba180796b34", size = 216497, upload-time = "2026-03-19T13:56:03.547Z" }, + { url = "https://files.pythonhosted.org/packages/89/a7/9f4977405d3a3fa02cf575951f03a9cda4c01efbe27a19230addee06acc2/pyroaring-1.0.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:924ee997ff1a0f2a184e39e153e9f77e0b928fe908d0aef63d03204c3ed90586", size = 322060, upload-time = "2026-03-19T13:56:04.94Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e9/32fd7125aea82a3d1d29b755edbc7bb531907638c68a5bcc767d20c2be4a/pyroaring-1.0.4-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:b7a527279cc378e893a543a2271d71321b57d21733915a5a14e587532e29265a", size = 685716, upload-time = "2026-03-19T13:56:06.431Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b3/6b78bd9d743c053fb3b0485a6b1f487e6a658123f06013bee55610b02120/pyroaring-1.0.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:d73979e1a3a6de2b7039dd9a545afa23f3b33f8b6f90a825390a34253d097f96", size = 363373, upload-time = "2026-03-19T13:56:07.707Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ff/188c16cdd75d841e50d2779ff7b5d1c5c915a6f23006ef3ab3680f48faca/pyroaring-1.0.4-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dab3d8577b143c64c1c1659b4d1c69d7fec5baa0d0c0181cdddf6b84f43af00a", size = 1914865, upload-time = "2026-03-19T13:56:09.22Z" }, + { url = "https://files.pythonhosted.org/packages/c8/12/ae7b4fa3682190597cbfb252be570358c5bc55f46f6b05db3fde66dfacc1/pyroaring-1.0.4-cp312-cp312-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dcc7d0133f46163b5390dd151e3305bd47289f7710c6e5444f38453d55b15d1c", size = 1742423, upload-time = "2026-03-19T13:56:10.422Z" }, + { url = "https://files.pythonhosted.org/packages/f7/25/966ea0a9d857ac3f2af1eaebdbfd56e627507b890f8ff7752c32e8e57b57/pyroaring-1.0.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a8b8448c2f7af3b40f17dde23b6739f3b19cf8b24db6f817c549c870d50aae3", size = 2130698, upload-time = "2026-03-19T13:56:12.039Z" }, + { url = "https://files.pythonhosted.org/packages/7a/31/0d0320925cf8bc1fcb53db182359bd673bf6b434f31c6cc69e4f5312c55f/pyroaring-1.0.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:13ccc488cfe6a227945586090397f299fd40c1a0dc1ed25e8e58a4d068c1ed46", size = 2822746, upload-time = "2026-03-19T13:56:13.274Z" }, + { url = "https://files.pythonhosted.org/packages/b9/98/a5f6d619098e307baf71b14a4df955914e8092f459d19daf80fbbd651fa1/pyroaring-1.0.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:dd89ebb7496325fb1b3dbe290dad35bc4da8722b5e3afd2a71b1d6e8ad981725", size = 2657370, upload-time = "2026-03-19T13:56:14.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/13/4d8beb31e4f648326b9b39c8f056fc1cb41422ef4e2be17cb15432c7fd40/pyroaring-1.0.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06b15bc8e0c272c3bee0c8adc29130178cd792ad99af64d7d7a1eb3069a1e0b8", size = 3088618, upload-time = "2026-03-19T13:56:16.625Z" }, + { url = "https://files.pythonhosted.org/packages/ad/16/95e223db5e60a6def8abcc2a8ebc4c71df23148a602310973c9a6964e3c5/pyroaring-1.0.4-cp312-cp312-win32.whl", hash = "sha256:4dca094f1d0e18901fa3f6b8866fa14a0f9640b22f41f5fc278c20e15e70efee", size = 202455, upload-time = "2026-03-19T13:56:18.124Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2c/5e41a91822c3bbc735382939d354e2c10bae453b18fe5a133f7fdbb33ce9/pyroaring-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:63ab0dcb24933fc9d4ca8c9fa0440f7e177183975990f756a42cddad22fd66db", size = 257479, upload-time = "2026-03-19T13:56:19.25Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d7/7bc58e807e7d6739f4f5c45964a35927e1ff7f591e3943372097d29a00a7/pyroaring-1.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:226079645dd4098d3619ae5fc19bf9abfd2187a74aba94a8768443e637d406fa", size = 215516, upload-time = "2026-03-19T13:56:20.286Z" }, + { url = "https://files.pythonhosted.org/packages/45/d2/bd78613ecab5dcc17631de0de019b16470f1974e88cfb577caf131083661/pyroaring-1.0.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4171902a88df936564f39cce05e36ff5d363e122575d5cf575163917be9ec91c", size = 321192, upload-time = "2026-03-19T13:56:21.605Z" }, + { url = "https://files.pythonhosted.org/packages/be/25/9c1c57743c2bb400d09e29f174b5369054e2700343b04dce36bdb55702e3/pyroaring-1.0.4-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:cbbe3fd3baef0bf04e149031aff44485757173370c5ce326ccb5269e131377ed", size = 684346, upload-time = "2026-03-19T13:56:22.996Z" }, + { url = "https://files.pythonhosted.org/packages/36/10/3c8cdc657b7ca4f30f51e385980b205bf1b1f04cad12c856a34db380bc5a/pyroaring-1.0.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:f38ca8a8b43f6a12d606bf68d9c0348620bca29b97d315d545caa3f0a1179902", size = 362544, upload-time = "2026-03-19T13:56:24.068Z" }, + { url = "https://files.pythonhosted.org/packages/86/46/3c6177117baa1896f38818767db1cd43130b32afc8b63a7be2a0c18faab6/pyroaring-1.0.4-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2ef35040b1bc4bb686b4206f4a4de748f6209fa578cafdf177ca3bb67f7bd552", size = 1914120, upload-time = "2026-03-19T13:56:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/f3/84/61176b1fdda1cde8572fc66fd88c1169f72fcace6b443bd8113df075b940/pyroaring-1.0.4-cp313-cp313-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:15c7f8b4bfc66c005723218500af414466a52e5cd419dbdd3a899307303b4322", size = 1741664, upload-time = "2026-03-19T13:56:27.369Z" }, + { url = "https://files.pythonhosted.org/packages/48/8c/fc091d54a09f549c6fb860302676f2b3ca79ffbda23f298e59bdba693989/pyroaring-1.0.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbed00fb4c4f2ba0446e159f025beebb12c2f12dd34827551eb239438044fdb9", size = 2107765, upload-time = "2026-03-19T13:56:28.605Z" }, + { url = "https://files.pythonhosted.org/packages/95/d4/864caa6719c3c229767a1cc6c32f707c0f7a67dbe19c3be595c7f43dec4d/pyroaring-1.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79af77dfb872da8a908514628b3f6bbcd2151bf31aeafebe2527e8d6fb786c97", size = 2819767, upload-time = "2026-03-19T13:56:30.216Z" }, + { url = "https://files.pythonhosted.org/packages/3d/67/7b0c91ea00d0c3e447c7507d8e9f26d94cad1f3c24c86088840d9fc490d2/pyroaring-1.0.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:15b0b92b4dcdb636ecd0a2104ef3be6b5288f85642dd16d7d631152948c3856e", size = 2651293, upload-time = "2026-03-19T13:56:31.856Z" }, + { url = "https://files.pythonhosted.org/packages/a3/aa/1c3746750c8c9b972d421fc6eae27fc21109a5cc26b89b7d5f8d104a613d/pyroaring-1.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:68f18b8fffee671996a4ba450f015a89281f7b7516ccc70b1398f20f46858536", size = 3070775, upload-time = "2026-03-19T13:56:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/05/da/a527a6141ea8839bee27869b92584f05120e1fc47cd572e313ddf5fd9f56/pyroaring-1.0.4-cp313-cp313-win32.whl", hash = "sha256:9be9df4a9ec671fdef22581e8ac9110ca8c47c5724e90ce825d54813de8ac3d3", size = 202501, upload-time = "2026-03-19T13:56:35.473Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b1/4ab2b78ab46dd4312e68013f997b4e771ed91c6acb65a00bbeafe7ea3fe4/pyroaring-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:048b915bacfc8e4d05de4fe6c2d7f7dd9d4c7a200b3b23dc1589226ca2526b41", size = 257129, upload-time = "2026-03-19T13:56:36.746Z" }, + { url = "https://files.pythonhosted.org/packages/70/73/ba128b5bafe10f7098217f257a11b1f5ecb50947e3800101a13124562d0f/pyroaring-1.0.4-cp313-cp313-win_arm64.whl", hash = "sha256:645af7435e9ad31a7aa6891b55797fe904c9c579fc5993b7fd59605f593188b9", size = 215209, upload-time = "2026-03-19T13:56:37.785Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d9/64ae48d0d7d7e8352ab894a2d38b111db87499af2d2fa98464b395a1ab86/pyroaring-1.0.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d0b14587e4ea67b7c7e16293bd934929ac1a7cced34aed11caab15466b24b8d4", size = 322966, upload-time = "2026-03-19T13:56:38.926Z" }, + { url = "https://files.pythonhosted.org/packages/92/ee/573e103a721feb22b9299c65d71bfc24872ddc487a84b61407a45750cd4d/pyroaring-1.0.4-cp314-cp314-macosx_14_0_universal2.whl", hash = "sha256:55ae44a958017ef5d355c80a1b20ecd5cf8cd796ea26c9e34ef606a2f65f49de", size = 686037, upload-time = "2026-03-19T13:56:40.148Z" }, + { url = "https://files.pythonhosted.org/packages/c8/00/0c16fcd126e1cac0280ab1f44bcac8603ecca124c36cf5c523698abe628d/pyroaring-1.0.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:316f89f7961972d07b7ed71f32335fe1b47d143b3dca1c35d58c48299f687750", size = 363100, upload-time = "2026-03-19T13:56:42.184Z" }, + { url = "https://files.pythonhosted.org/packages/51/5d/1bc58754e2c747b06ff380467a2d38b63542856bbf681b4472fa3f9e2346/pyroaring-1.0.4-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:edd767e6263fa17feae959519ee3d9bdbd0c5f03b55f89ecdff9d7136886e8f6", size = 1913082, upload-time = "2026-03-19T13:56:43.334Z" }, + { url = "https://files.pythonhosted.org/packages/23/21/8f75c12cab6e0af84464e8d8aff14bc46daa72d8d28c1fa966ba5f296705/pyroaring-1.0.4-cp314-cp314-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2fc84c41de2b1370d57fea076f12246e6ab9333499ba6e8c74a5ea32168d357a", size = 1734580, upload-time = "2026-03-19T13:56:44.572Z" }, + { url = "https://files.pythonhosted.org/packages/32/30/aa3b93bbb13c9d122e6d95cebedcd3479366cee9d78094f5207b9da0642e/pyroaring-1.0.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abcb5083a80453ff59757882510c41e895b20c3326a37e3f683993953ae6ed1c", size = 2097410, upload-time = "2026-03-19T13:56:46.158Z" }, + { url = "https://files.pythonhosted.org/packages/74/ff/75e3a99ebc63ba2be3a484bd7b0e191209f4b5cbbcb094a057779cef4ac5/pyroaring-1.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:078ee25ed3fda098032d7f30a603f836b538e5e7e5717d8dee29b42de4547250", size = 2823136, upload-time = "2026-03-19T13:56:47.479Z" }, + { url = "https://files.pythonhosted.org/packages/78/1d/85d398b50f4e4c47ee88e65d172d07a4bc2f2d981ca4b366e5c83b67f38c/pyroaring-1.0.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ed967ec4ef3b9de7ef3f7c0cf3a850b599111479654d0b7962f12acea77b24ce", size = 2635231, upload-time = "2026-03-19T13:56:48.927Z" }, + { url = "https://files.pythonhosted.org/packages/fc/33/db0761c3120a65e8b2e0ee3d75b645674c532c65c22396a219f707bf963d/pyroaring-1.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2f617e66633428659193736bc568a0462f2966ab14229ec936ebb490647caa67", size = 3059892, upload-time = "2026-03-19T13:56:50.415Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2b/4d73d056a73f65af4a58e8c691adcbcf563dd220d21f4285ebcc7d10509c/pyroaring-1.0.4-cp314-cp314-win32.whl", hash = "sha256:f0239d70d5d83227fa43a43b332bf707c6cafa7e28c14889d1602fc872934f02", size = 207536, upload-time = "2026-03-19T13:56:52.335Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8e/bc8b4704742debc21b4f4eebb5de8fc7d9e3ce8643b7d65c5c519eff5e42/pyroaring-1.0.4-cp314-cp314-win_amd64.whl", hash = "sha256:06f080c8dc42af32f66f11afdc603d5bf965d9239ad8d7773f0bae0d28a4e22f", size = 264935, upload-time = "2026-03-19T13:56:53.364Z" }, + { url = "https://files.pythonhosted.org/packages/52/2e/f9146436ecf3cf5af8c581f69ebbf03dd3d1561c7b8fe39679b50a3c37b9/pyroaring-1.0.4-cp314-cp314-win_arm64.whl", hash = "sha256:9ef52a2a1b3e6ccb3daac2dd5e8178349846d05f8cc1d0c05c551f73927cf0b4", size = 223530, upload-time = "2026-03-19T13:56:54.463Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a9/f0a925c0d857d23830fa1a12452d0976f8ba245f2e4a9f019243b6d6f0c4/pyroaring-1.0.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:91e615d6a5e114ce9297650fabd48bf1f01655cfd5bd9cb41b9b1dc0ff840e06", size = 335434, upload-time = "2026-03-19T13:56:55.548Z" }, + { url = "https://files.pythonhosted.org/packages/66/36/0c06caab704ed4573e5e4f24e5f4a99c6c9c03f24a53d0f73970517dceff/pyroaring-1.0.4-cp314-cp314t-macosx_14_0_universal2.whl", hash = "sha256:d943aebe18f017e3a37acdf8eced5b37bd9b5d5fcde8d581936ec6732b72f3c7", size = 707199, upload-time = "2026-03-19T13:56:56.622Z" }, + { url = "https://files.pythonhosted.org/packages/07/4a/0fdd0bead1a3889cc6bd0dccbc168df5a4c9fc635e40a95f13426246c7c3/pyroaring-1.0.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:01f990499a4f34a666702da16080b6a94b990c5dc5cd7f2b1f6f92a7184b94ed", size = 372204, upload-time = "2026-03-19T13:56:57.762Z" }, + { url = "https://files.pythonhosted.org/packages/c9/8a/78cfd7c35c1e6a82bbca8e3f57f123273168b892ab92daf5138a3717e57d/pyroaring-1.0.4-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:236c5fb377d942c0911c7f7b1d929e192ad0165f07dca0957fa4b8ec607b4a8d", size = 1962093, upload-time = "2026-03-19T13:56:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/36/f7/12f5dc15217f9d018b0b76dafb89db49833c14d58e49a0a16779b7b231ea/pyroaring-1.0.4-cp314-cp314t-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:84925878cff40ce1f5a99de0fc5b49b7cf6a6276ae5046ae80bc2ae9b5b83d83", size = 1751588, upload-time = "2026-03-19T13:57:01.097Z" }, + { url = "https://files.pythonhosted.org/packages/f1/84/7291e0a04af887f58ecd58a304dae2f979a8393ad7cd18059a744a16053b/pyroaring-1.0.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4ce889a06603931a03195b90dc11f03cfb6849c4d8504168e7325da06ce95ee", size = 2130851, upload-time = "2026-03-19T13:57:02.38Z" }, + { url = "https://files.pythonhosted.org/packages/bc/23/56d4dba6fa2aae5c862f1c46d9f8c8d53f7c3ade95e327db7e430913ef8a/pyroaring-1.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f8285cb12c7181cd29c9807f624d729c0db7bbf6f96fe7c25aba719225f85051", size = 2853020, upload-time = "2026-03-19T13:57:03.663Z" }, + { url = "https://files.pythonhosted.org/packages/ba/25/9a3e61e85ce1e670fe3fbaac2a56eb0c8c8aa6ac3a945f5f734bf5851b7f/pyroaring-1.0.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:731e1f64292ee0e99cf43ef9d052c75c06f4c6dda752a6a21d062726bb077a2b", size = 2645177, upload-time = "2026-03-19T13:57:05.263Z" }, + { url = "https://files.pythonhosted.org/packages/25/43/f57f8696c050dd8b642522e9ab775987c8fec7a61ab2a0899776c7f82036/pyroaring-1.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4035852113b720f72872741be8f9760c0d2dd1640f0bc598ece7ff7ca226a3a0", size = 3087856, upload-time = "2026-03-19T13:57:06.531Z" }, + { url = "https://files.pythonhosted.org/packages/74/3a/0a9d837478912ce51d1583bc4352818e8933252207d05d3021e91103e084/pyroaring-1.0.4-cp314-cp314t-win32.whl", hash = "sha256:f9a111e668f026849a22a7e4cab628e41e48bb87bd2a9aea72bca75edcd7a6f2", size = 234160, upload-time = "2026-03-19T13:57:07.848Z" }, + { url = "https://files.pythonhosted.org/packages/50/9d/bcd304935f20ce5f9ecb2837df69516be657e144da350a73a1899985aa38/pyroaring-1.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:92fdad43421ad2e076639f7a0d9ee815713b98bfe07dd9107ef3ea91ff0f3b37", size = 298468, upload-time = "2026-03-19T13:57:08.94Z" }, + { url = "https://files.pythonhosted.org/packages/a6/4d/bd52a55dcef5cde4d0dda8c80000463c6929c7faba2466d9fa58fd0d48f8/pyroaring-1.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:c291428f148450e0609d5b1201b81e8248b3262770e51fc4c89768529ee36df0", size = 234175, upload-time = "2026-03-19T13:57:10.071Z" }, + { url = "https://files.pythonhosted.org/packages/ca/dd/fe18c9eff364ab5d85ca5b5db408faddb13ca6c99211718c7c7c18fca689/pyroaring-1.0.4-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:457dbadc044ad5ceb65dbdbc9aabc2ef557d4177353a7705fbe786a8336aba81", size = 320894, upload-time = "2026-03-19T13:57:11.454Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d1/2686989628cc6fdd37a2ccdd8a89f38b4c7f2d15e9d57a271b524479dac3/pyroaring-1.0.4-cp39-cp39-macosx_14_0_universal2.whl", hash = "sha256:c461121b1ee5462ca0da0e5f67d836f04d29b140e4b26969647347e2e89bc058", size = 682540, upload-time = "2026-03-19T13:57:12.583Z" }, + { url = "https://files.pythonhosted.org/packages/3f/39/42e515d778abcbfde8e3efd4e3a700d01d9577d88a063f7df66b1ce32fba/pyroaring-1.0.4-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:474b25774e0ea8e36ad3f811dbec6183ba4bcf93ee95674a14a4ae96932f0ffd", size = 400256, upload-time = "2026-03-19T13:57:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e7/6f3570a34bafa1edbfef87b4ce4eb923fb3249141f9e18eed46ef50ad7fb/pyroaring-1.0.4-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f8e82d665385033eef1794d96a34cfb34da76fa6896d49756510f2f32a0c218", size = 1881153, upload-time = "2026-03-19T13:57:15.223Z" }, + { url = "https://files.pythonhosted.org/packages/af/a5/9f09bdb25c2bd58936419598e4ded328947c929a2fbaa5a8acd4ce47ff53/pyroaring-1.0.4-cp39-cp39-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cac139cbeb1941760a9568d742d29bd31eaa2ea7bb6cf54757d14be4498145b9", size = 1746808, upload-time = "2026-03-19T13:57:16.427Z" }, + { url = "https://files.pythonhosted.org/packages/2a/be/9de29d2ec8080e8bff0ba44f2429e30025f4c838d526b9b315756e17c3cf/pyroaring-1.0.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c67b59f5f794e3b8262b89148a2662199a11aac4169cd5515c6dc74efa7a3447", size = 2073909, upload-time = "2026-03-19T13:57:18.05Z" }, + { url = "https://files.pythonhosted.org/packages/41/dd/4898378a555b218630a6d5be8e278d19b24b356075ec92e03e0c8ea090c7/pyroaring-1.0.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:598f4f4f2d61f2c5e5e64319faa03866397ab74a9bb7ef99d14ecd9f1a768166", size = 2806003, upload-time = "2026-03-19T13:57:19.655Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a1/13fe01e90e31c135a2b34d87b2f66a87bc3ade61d968276b3128d3878e16/pyroaring-1.0.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:3487b1531f3532f8a024a5b8a9fac97bd1696c4df2a0ca92aa9e07009fcd0a9c", size = 2605100, upload-time = "2026-03-19T13:57:21.124Z" }, + { url = "https://files.pythonhosted.org/packages/52/c7/e40ecc9bd5c7d3c723b63563966ade47fe0ed6aa81ed85bdb3acca960fab/pyroaring-1.0.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9a438455b90bf28ea5939ea35af8926e944f9c1480dcce3feea38a0239fe6629", size = 3045090, upload-time = "2026-03-19T13:57:22.437Z" }, + { url = "https://files.pythonhosted.org/packages/54/05/08a17f5568f3cd3680dcbf4f4314c6071ab3f82c8999f86a36c7dd222f9b/pyroaring-1.0.4-cp39-cp39-win32.whl", hash = "sha256:310495c93d8e97b07de9024b6439215a78a5cc165e103a7cda7bf183572a4329", size = 203126, upload-time = "2026-03-19T13:57:24.026Z" }, + { url = "https://files.pythonhosted.org/packages/29/00/c653b5d337db22a5d5cd361828169c182a1dbe55cf339d0b9731c5e9a29d/pyroaring-1.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:7dfc36328b9dcd36ab2ba6c1d3438fa8503d9f375fa29786606dcca43fd0c94c", size = 252673, upload-time = "2026-03-19T13:57:25.115Z" }, + { url = "https://files.pythonhosted.org/packages/d1/bc/17ecfd5e1788901f0dce1b4c4e9e328013c5e85b854958e53054fcb7890b/pyroaring-1.0.4-cp39-cp39-win_arm64.whl", hash = "sha256:ae8334787aabb41b947d203c4430f3589d587b2d65809bac80850dbdc65fcea7", size = 216863, upload-time = "2026-03-19T13:57:26.162Z" }, +] + [[package]] name = "pytest" version = "8.4.2" @@ -4192,6 +4920,74 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] +[[package]] +name = "sqlalchemy" +version = "2.0.49" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", version = "3.2.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.10' and platform_machine == 'AMD64') or (python_full_version < '3.10' and platform_machine == 'WIN32') or (python_full_version < '3.10' and platform_machine == 'aarch64') or (python_full_version < '3.10' and platform_machine == 'amd64') or (python_full_version < '3.10' and platform_machine == 'ppc64le') or (python_full_version < '3.10' and platform_machine == 'win32') or (python_full_version < '3.10' and platform_machine == 'x86_64')" }, + { name = "greenlet", version = "3.3.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.10' and platform_machine == 'AMD64') or (python_full_version >= '3.10' and platform_machine == 'WIN32') or (python_full_version >= '3.10' and platform_machine == 'aarch64') or (python_full_version >= '3.10' and platform_machine == 'amd64') or (python_full_version >= '3.10' and platform_machine == 'ppc64le') or (python_full_version >= '3.10' and platform_machine == 'win32') or (python_full_version >= '3.10' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/76/f908955139842c362aa877848f42f9249642d5b69e06cee9eae5111da1bd/sqlalchemy-2.0.49-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:42e8804962f9e6f4be2cbaedc0c3718f08f60a16910fa3d86da5a1e3b1bfe60f", size = 2159321, upload-time = "2026-04-03T16:50:11.8Z" }, + { url = "https://files.pythonhosted.org/packages/24/e2/17ba0b7bfbd8de67196889b6d951de269e8a46057d92baca162889beb16d/sqlalchemy-2.0.49-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc992c6ed024c8c3c592c5fc9846a03dd68a425674900c70122c77ea16c5fb0b", size = 3238937, upload-time = "2026-04-03T16:54:45.731Z" }, + { url = "https://files.pythonhosted.org/packages/90/1e/410dd499c039deacff395eec01a9da057125fcd0c97e3badc252c6a2d6a7/sqlalchemy-2.0.49-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eb188b84269f357669b62cb576b5b918de10fb7c728a005fa0ebb0b758adce1", size = 3237188, upload-time = "2026-04-03T16:56:53.217Z" }, + { url = "https://files.pythonhosted.org/packages/ab/06/e797a8b98a3993ac4bc785309b9b6d005457fc70238ee6cefa7c8867a92e/sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:62557958002b69699bdb7f5137c6714ca1133f045f97b3903964f47db97ea339", size = 3190061, upload-time = "2026-04-03T16:54:47.489Z" }, + { url = "https://files.pythonhosted.org/packages/44/d3/5a9f7ef580af1031184b38235da6ac58c3b571df01c9ec061c44b2b0c5a6/sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da9b91bca419dc9b9267ffadde24eae9b1a6bffcd09d0a207e5e3af99a03ce0d", size = 3211477, upload-time = "2026-04-03T16:56:55.056Z" }, + { url = "https://files.pythonhosted.org/packages/69/ec/7be8c8cb35f038e963a203e4fe5a028989167cc7299927b7cf297c271e37/sqlalchemy-2.0.49-cp310-cp310-win32.whl", hash = "sha256:5e61abbec255be7b122aa461021daa7c3f310f3e743411a67079f9b3cc91ece3", size = 2119965, upload-time = "2026-04-03T17:00:50.009Z" }, + { url = "https://files.pythonhosted.org/packages/b5/31/0defb93e3a10b0cf7d1271aedd87251a08c3a597ee4f353281769b547b5a/sqlalchemy-2.0.49-cp310-cp310-win_amd64.whl", hash = "sha256:0c98c59075b890df8abfcc6ad632879540f5791c68baebacb4f833713b510e75", size = 2142935, upload-time = "2026-04-03T17:00:51.675Z" }, + { url = "https://files.pythonhosted.org/packages/60/b5/e3617cc67420f8f403efebd7b043128f94775e57e5b84e7255203390ceae/sqlalchemy-2.0.49-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5070135e1b7409c4161133aa525419b0062088ed77c92b1da95366ec5cbebbe", size = 2159126, upload-time = "2026-04-03T16:50:13.242Z" }, + { url = "https://files.pythonhosted.org/packages/20/9b/91ca80403b17cd389622a642699e5f6564096b698e7cdcbcbb6409898bc4/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ac7a3e245fd0310fd31495eb61af772e637bdf7d88ee81e7f10a3f271bff014", size = 3315509, upload-time = "2026-04-03T16:54:49.332Z" }, + { url = "https://files.pythonhosted.org/packages/b1/61/0722511d98c54de95acb327824cb759e8653789af2b1944ab1cc69d32565/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d4e5a0ceba319942fa6b585cf82539288a61e314ef006c1209f734551ab9536", size = 3315014, upload-time = "2026-04-03T16:56:56.376Z" }, + { url = "https://files.pythonhosted.org/packages/46/55/d514a653ffeb4cebf4b54c47bec32ee28ad89d39fafba16eeed1d81dccd5/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ddcb27fb39171de36e207600116ac9dfd4ae46f86c82a9bf3934043e80ebb88", size = 3267388, upload-time = "2026-04-03T16:54:51.272Z" }, + { url = "https://files.pythonhosted.org/packages/2f/16/0dcc56cb6d3335c1671a2258f5d2cb8267c9a2260e27fde53cbfb1b3540a/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:32fe6a41ad97302db2931f05bb91abbcc65b5ce4c675cd44b972428dd2947700", size = 3289602, upload-time = "2026-04-03T16:56:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/51/6c/f8ab6fb04470a133cd80608db40aa292e6bae5f162c3a3d4ab19544a67af/sqlalchemy-2.0.49-cp311-cp311-win32.whl", hash = "sha256:46d51518d53edfbe0563662c96954dc8fcace9832332b914375f45a99b77cc9a", size = 2119044, upload-time = "2026-04-03T17:00:53.455Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/55a6d627d04b6ebb290693681d7683c7da001eddf90b60cfcc41ee907978/sqlalchemy-2.0.49-cp311-cp311-win_amd64.whl", hash = "sha256:951d4a210744813be63019f3df343bf233b7432aadf0db54c75802247330d3af", size = 2143642, upload-time = "2026-04-03T17:00:54.769Z" }, + { url = "https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b", size = 2157681, upload-time = "2026-04-03T16:53:07.132Z" }, + { url = "https://files.pythonhosted.org/packages/50/84/b2a56e2105bd11ebf9f0b93abddd748e1a78d592819099359aa98134a8bf/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982", size = 3338976, upload-time = "2026-04-03T17:07:40Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672", size = 3351937, upload-time = "2026-04-03T17:12:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2f/6fd118563572a7fe475925742eb6b3443b2250e346a0cc27d8d408e73773/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e", size = 3281646, upload-time = "2026-04-03T17:07:41.949Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d7/410f4a007c65275b9cf82354adb4bb8ba587b176d0a6ee99caa16fe638f8/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750", size = 3316695, upload-time = "2026-04-03T17:12:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/d9/95/81f594aa60ded13273a844539041ccf1e66c5a7bed0a8e27810a3b52d522/sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0", size = 2117483, upload-time = "2026-04-03T17:05:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4", size = 2144494, upload-time = "2026-04-03T17:05:42.282Z" }, + { url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547, upload-time = "2026-04-03T16:53:08.64Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782, upload-time = "2026-04-03T17:07:43.508Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156, upload-time = "2026-04-03T17:12:27.697Z" }, + { url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832, upload-time = "2026-04-03T17:07:45.38Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000, upload-time = "2026-04-03T17:12:29.657Z" }, + { url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641, upload-time = "2026-04-03T17:05:43.989Z" }, + { url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498, upload-time = "2026-04-03T17:05:45.7Z" }, + { url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807, upload-time = "2026-04-03T16:58:31.666Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481, upload-time = "2026-04-03T17:06:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565, upload-time = "2026-04-03T16:58:33.414Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769, upload-time = "2026-04-03T17:06:02.787Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319, upload-time = "2026-04-03T17:02:04.328Z" }, + { url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104, upload-time = "2026-04-03T17:02:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e", size = 2156356, upload-time = "2026-04-03T16:53:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a7/5f476227576cb8644650eff68cc35fa837d3802b997465c96b8340ced1e2/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a", size = 3276486, upload-time = "2026-04-03T17:07:46.9Z" }, + { url = "https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066", size = 3281479, upload-time = "2026-04-03T17:12:32.226Z" }, + { url = "https://files.pythonhosted.org/packages/91/68/bb406fa4257099c67bd75f3f2261b129c63204b9155de0d450b37f004698/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187", size = 3226269, upload-time = "2026-04-03T17:07:48.678Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/acb56c00cca9f251f437cb49e718e14f7687505749ea9255d7bd8158a6df/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401", size = 3248260, upload-time = "2026-04-03T17:12:34.381Z" }, + { url = "https://files.pythonhosted.org/packages/56/19/6a20ea25606d1efd7bd1862149bb2a22d1451c3f851d23d887969201633f/sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5", size = 2118463, upload-time = "2026-04-03T17:05:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5", size = 2144204, upload-time = "2026-04-03T17:05:48.694Z" }, + { url = "https://files.pythonhosted.org/packages/1f/33/95e7216df810c706e0cd3655a778604bbd319ed4f43333127d465a46862d/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977", size = 3565474, upload-time = "2026-04-03T16:58:35.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a4/ed7b18d8ccf7f954a83af6bb73866f5bc6f5636f44c7731fbb741f72cc4f/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01", size = 3530567, upload-time = "2026-04-03T17:06:04.587Z" }, + { url = "https://files.pythonhosted.org/packages/73/a3/20faa869c7e21a827c4a2a42b41353a54b0f9f5e96df5087629c306df71e/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61", size = 3474282, upload-time = "2026-04-03T16:58:37.131Z" }, + { url = "https://files.pythonhosted.org/packages/b7/50/276b9a007aa0764304ad467eceb70b04822dc32092492ee5f322d559a4dc/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a", size = 3480406, upload-time = "2026-04-03T17:06:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c3/c80fcdb41905a2df650c2a3e0337198b6848876e63d66fe9188ef9003d24/sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158", size = 2149151, upload-time = "2026-04-03T17:02:07.281Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9f1a62feab6ed368aff068524ff414f26a6daebc7361861035ae00b05530/sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7", size = 2184178, upload-time = "2026-04-03T17:02:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/1d/64/6eb36149b96796ecbc1e2438959d08475e1f8765acbe007f4785a603c39c/sqlalchemy-2.0.49-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43d044780732d9e0381ac8d5316f95d7f02ef04d6e4ef6dc82379f09795d993f", size = 2162373, upload-time = "2026-04-03T16:49:49.55Z" }, + { url = "https://files.pythonhosted.org/packages/b0/96/87e57cfa06af0032a7470660d33e93ad0a2480781bb7705f4312471b993e/sqlalchemy-2.0.49-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d6be30b2a75362325176c036d7fb8d19e8846c77e87683ffaa8177b35135613", size = 3237991, upload-time = "2026-04-03T17:04:07.027Z" }, + { url = "https://files.pythonhosted.org/packages/b7/aa/0099d0d554313c3587155b60288a9900660afc9989bf382176a5f4d7531b/sqlalchemy-2.0.49-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d898cc2c76c135ef65517f4ddd7a3512fb41f23087b0650efb3418b8389a3cd1", size = 3237313, upload-time = "2026-04-03T17:09:53.187Z" }, + { url = "https://files.pythonhosted.org/packages/d5/9b/a61fcb2e8439a2282e4ac0086bb613e88cd18168cddb358fa2c5790d4705/sqlalchemy-2.0.49-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:059d7151fff513c53a4638da8778be7fce81a0c4854c7348ebd0c4078ddf28fe", size = 3187435, upload-time = "2026-04-03T17:04:08.956Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/2165d3f8fa593f20039505af15474f63e85ffd7998afb6218b0fc0cd98e0/sqlalchemy-2.0.49-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:334edbcff10514ad1d66e3a70b339c0a29886394892490119dbb669627b17717", size = 3209446, upload-time = "2026-04-03T17:09:55.81Z" }, + { url = "https://files.pythonhosted.org/packages/23/8d/9630ddc9a4db638a7f29954b9e667a4ece41ff65e117460473ca41f06945/sqlalchemy-2.0.49-cp39-cp39-win32.whl", hash = "sha256:74ab4ee7794d7ed1b0c37e7333640e0f0a626fc7b398c07a7aef52f484fddde3", size = 2121680, upload-time = "2026-04-03T16:55:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5c/480f5d8c737cfb4a494f87de6e0e58a6b6346a0f4db1fa8122c89828e32d/sqlalchemy-2.0.49-cp39-cp39-win_amd64.whl", hash = "sha256:88690f4e1f0fbf5339bedbb127e240fec1fd3070e9934c0b7bef83432f779d2f", size = 2144917, upload-time = "2026-04-03T16:55:24.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" }, +] + [[package]] name = "stack-data" version = "0.6.3" @@ -4206,6 +5002,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] +[[package]] +name = "strictyaml" +version = "1.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/08/efd28d49162ce89c2ad61a88bd80e11fb77bc9f6c145402589112d38f8af/strictyaml-1.7.3.tar.gz", hash = "sha256:22f854a5fcab42b5ddba8030a0e4be51ca89af0267961c8d6cfa86395586c407", size = 115206, upload-time = "2023-03-10T12:50:27.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/7c/a81ef5ef10978dd073a854e0fa93b5d8021d0594b639cc8f6453c3c78a1d/strictyaml-1.7.3-py3-none-any.whl", hash = "sha256:fb5c8a4edb43bebb765959e420f9b3978d7f1af88c80606c03fb420888f5d1c7", size = 123917, upload-time = "2023-03-10T12:50:17.242Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + [[package]] name = "tomli" version = "2.3.0" @@ -4296,6 +5130,14 @@ all = [ { name = "polars" }, { name = "pyarrow", version = "21.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pyarrow", version = "22.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyiceberg", version = "0.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyiceberg", version = "0.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "sqlalchemy" }, +] +catalog = [ + { name = "pyiceberg", version = "0.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyiceberg", version = "0.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "sqlalchemy" }, ] pandas = [ { name = "pandas" }, @@ -4333,10 +5175,13 @@ all = [ { name = "pre-commit", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pyarrow", version = "21.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pyarrow", version = "22.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyiceberg", version = "0.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyiceberg", version = "0.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-benchmark" }, { name = "ruff" }, + { name = "sqlalchemy" }, ] benchmarks = [ { name = "fast-forward-football", marker = "python_full_version == '3.11.*'" }, @@ -4348,9 +5193,12 @@ benchmarks = [ { name = "pre-commit", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pyarrow", version = "21.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pyarrow", version = "22.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyiceberg", version = "0.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyiceberg", version = "0.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-benchmark" }, + { name = "sqlalchemy" }, ] dev = [ { name = "maturin" }, @@ -4360,6 +5208,9 @@ dev = [ { name = "pre-commit", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pyarrow", version = "21.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pyarrow", version = "22.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyiceberg", version = "0.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyiceberg", version = "0.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "sqlalchemy" }, ] docs = [ { name = "maturin" }, @@ -4370,6 +5221,9 @@ docs = [ { name = "pre-commit", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pyarrow", version = "21.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pyarrow", version = "22.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyiceberg", version = "0.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyiceberg", version = "0.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "sqlalchemy" }, ] linting = [ { name = "maturin" }, @@ -4383,7 +5237,10 @@ linting = [ { name = "pre-commit", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pyarrow", version = "21.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pyarrow", version = "22.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyiceberg", version = "0.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyiceberg", version = "0.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "ruff" }, + { name = "sqlalchemy" }, ] nb = [ { name = "huggingface-hub" }, @@ -4402,9 +5259,12 @@ testing = [ { name = "pre-commit", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pyarrow", version = "21.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pyarrow", version = "22.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyiceberg", version = "0.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyiceberg", version = "0.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-benchmark" }, + { name = "sqlalchemy" }, ] [package.metadata] @@ -4416,8 +5276,12 @@ requires-dist = [ { name = "pyarrow", marker = "extra == 'all'", specifier = ">=8.0.0" }, { name = "pyarrow", marker = "extra == 'pandas'", specifier = ">=8.0.0" }, { name = "pyarrow", marker = "extra == 'pyarrow'", specifier = ">=8.0.0" }, + { name = "pyiceberg", marker = "extra == 'all'", specifier = ">=0.8.1" }, + { name = "pyiceberg", marker = "extra == 'catalog'", specifier = ">=0.8.1" }, + { name = "sqlalchemy", marker = "extra == 'all'", specifier = ">=2.0.0" }, + { name = "sqlalchemy", marker = "extra == 'catalog'", specifier = ">=2.0.0" }, ] -provides-extras = ["all", "pandas", "polars", "pyarrow"] +provides-extras = ["all", "catalog", "pandas", "polars", "pyarrow"] [package.metadata.requires-dev] all = [ @@ -4435,9 +5299,11 @@ all = [ { name = "polars", specifier = ">=0.16.14" }, { name = "pre-commit", specifier = ">=4.3.0" }, { name = "pyarrow", specifier = ">=8.0.0" }, + { name = "pyiceberg", specifier = ">=0.8.1" }, { name = "pytest", specifier = ">=7.1.3" }, { name = "pytest-benchmark", specifier = ">=4.0.0,<6" }, { name = "ruff", specifier = ">=0.12,<0.15" }, + { name = "sqlalchemy", specifier = ">=2.0.0" }, ] benchmarks = [ { name = "fast-forward-football", marker = "python_full_version == '3.11.*'", specifier = ">=0.1.1" }, @@ -4447,8 +5313,10 @@ benchmarks = [ { name = "polars", specifier = ">=0.16.14" }, { name = "pre-commit", specifier = ">=4.3.0" }, { name = "pyarrow", specifier = ">=8.0.0" }, + { name = "pyiceberg", specifier = ">=0.8.1" }, { name = "pytest", specifier = ">=7.1.3" }, { name = "pytest-benchmark", specifier = ">=4.0.0,<6" }, + { name = "sqlalchemy", specifier = ">=2.0.0" }, ] dev = [ { name = "maturin", specifier = ">=1.7.0,<2.0" }, @@ -4456,6 +5324,8 @@ dev = [ { name = "polars", specifier = ">=0.16.14" }, { name = "pre-commit", specifier = ">=4.3.0" }, { name = "pyarrow", specifier = ">=8.0.0" }, + { name = "pyiceberg", specifier = ">=0.8.1" }, + { name = "sqlalchemy", specifier = ">=2.0.0" }, ] docs = [ { name = "maturin", specifier = ">=1.7.0,<2.0" }, @@ -4464,6 +5334,8 @@ docs = [ { name = "polars", specifier = ">=0.16.14" }, { name = "pre-commit", specifier = ">=4.3.0" }, { name = "pyarrow", specifier = ">=8.0.0" }, + { name = "pyiceberg", specifier = ">=0.8.1" }, + { name = "sqlalchemy", specifier = ">=2.0.0" }, ] linting = [ { name = "maturin", specifier = ">=1.7.0,<2.0" }, @@ -4473,7 +5345,9 @@ linting = [ { name = "polars", specifier = ">=0.16.14" }, { name = "pre-commit", specifier = ">=4.3.0" }, { name = "pyarrow", specifier = ">=8.0.0" }, + { name = "pyiceberg", specifier = ">=0.8.1" }, { name = "ruff", specifier = ">=0.12,<0.15" }, + { name = "sqlalchemy", specifier = ">=2.0.0" }, ] nb = [ { name = "huggingface-hub", specifier = ">=1.6.0" }, @@ -4487,8 +5361,10 @@ testing = [ { name = "polars", specifier = ">=0.16.14" }, { name = "pre-commit", specifier = ">=4.3.0" }, { name = "pyarrow", specifier = ">=8.0.0" }, + { name = "pyiceberg", specifier = ">=0.8.1" }, { name = "pytest", specifier = ">=7.1.3" }, { name = "pytest-benchmark", specifier = ">=4.0.0,<6" }, + { name = "sqlalchemy", specifier = ">=2.0.0" }, ] [[package]] @@ -4571,6 +5447,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + [[package]] name = "tzdata" version = "2025.2" @@ -4914,3 +5802,109 @@ sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50e wheels = [ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, ] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/28efd1d371f1acd037ac64ed1c5e2b41514a6cc937dd6ab6a13ab9f0702f/zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd", size = 795256, upload-time = "2025-09-14T22:15:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/96/34/ef34ef77f1ee38fc8e4f9775217a613b452916e633c4f1d98f31db52c4a5/zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7", size = 640565, upload-time = "2025-09-14T22:15:58.177Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1b/4fdb2c12eb58f31f28c4d28e8dc36611dd7205df8452e63f52fb6261d13e/zstandard-0.25.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550", size = 5345306, upload-time = "2025-09-14T22:16:00.165Z" }, + { url = "https://files.pythonhosted.org/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d", size = 5055561, upload-time = "2025-09-14T22:16:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/e9/74/68341185a4f32b274e0fc3410d5ad0750497e1acc20bd0f5b5f64ce17785/zstandard-0.25.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b", size = 5402214, upload-time = "2025-09-14T22:16:04.109Z" }, + { url = "https://files.pythonhosted.org/packages/8b/67/f92e64e748fd6aaffe01e2b75a083c0c4fd27abe1c8747fee4555fcee7dd/zstandard-0.25.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0", size = 5449703, upload-time = "2025-09-14T22:16:06.312Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e5/6d36f92a197c3c17729a2125e29c169f460538a7d939a27eaaa6dcfcba8e/zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0", size = 5556583, upload-time = "2025-09-14T22:16:08.457Z" }, + { url = "https://files.pythonhosted.org/packages/d7/83/41939e60d8d7ebfe2b747be022d0806953799140a702b90ffe214d557638/zstandard-0.25.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd", size = 5045332, upload-time = "2025-09-14T22:16:10.444Z" }, + { url = "https://files.pythonhosted.org/packages/b3/87/d3ee185e3d1aa0133399893697ae91f221fda79deb61adbe998a7235c43f/zstandard-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701", size = 5572283, upload-time = "2025-09-14T22:16:12.128Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1d/58635ae6104df96671076ac7d4ae7816838ce7debd94aecf83e30b7121b0/zstandard-0.25.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1", size = 4959754, upload-time = "2025-09-14T22:16:14.225Z" }, + { url = "https://files.pythonhosted.org/packages/75/d6/57e9cb0a9983e9a229dd8fd2e6e96593ef2aa82a3907188436f22b111ccd/zstandard-0.25.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150", size = 5266477, upload-time = "2025-09-14T22:16:16.343Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a9/ee891e5edf33a6ebce0a028726f0bbd8567effe20fe3d5808c42323e8542/zstandard-0.25.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab", size = 5440914, upload-time = "2025-09-14T22:16:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/58/08/a8522c28c08031a9521f27abc6f78dbdee7312a7463dd2cfc658b813323b/zstandard-0.25.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e", size = 5819847, upload-time = "2025-09-14T22:16:20.559Z" }, + { url = "https://files.pythonhosted.org/packages/6f/11/4c91411805c3f7b6f31c60e78ce347ca48f6f16d552fc659af6ec3b73202/zstandard-0.25.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74", size = 5363131, upload-time = "2025-09-14T22:16:22.206Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d6/8c4bd38a3b24c4c7676a7a3d8de85d6ee7a983602a734b9f9cdefb04a5d6/zstandard-0.25.0-cp310-cp310-win32.whl", hash = "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa", size = 436469, upload-time = "2025-09-14T22:16:25.002Z" }, + { url = "https://files.pythonhosted.org/packages/93/90/96d50ad417a8ace5f841b3228e93d1bb13e6ad356737f42e2dde30d8bd68/zstandard-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e", size = 506100, upload-time = "2025-09-14T22:16:23.569Z" }, + { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, + { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, + { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, + { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, + { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, + { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/d0a405dad6ab6f9f759c26d866cca66cb209bff6f8db656074d662a953dd/zstandard-0.25.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b9af1fe743828123e12b41dd8091eca1074d0c1569cc42e6e1eee98027f2bbd0", size = 795263, upload-time = "2025-09-14T22:18:21.683Z" }, + { url = "https://files.pythonhosted.org/packages/ca/aa/ceb8d79cbad6dabd4cb1178ca853f6a4374d791c5e0241a0988173e2a341/zstandard-0.25.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b14abacf83dfb5c25eb4e4a79520de9e7e205f72c9ee7702f91233ae57d33a2", size = 640560, upload-time = "2025-09-14T22:18:22.867Z" }, + { url = "https://files.pythonhosted.org/packages/88/cd/2cf6d476131b509cc122d25d3416a2d0aa17687ddbada7599149f9da620e/zstandard-0.25.0-cp39-cp39-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:a51ff14f8017338e2f2e5dab738ce1ec3b5a851f23b18c1ae1359b1eecbee6df", size = 5344244, upload-time = "2025-09-14T22:18:24.724Z" }, + { url = "https://files.pythonhosted.org/packages/5c/71/e14820b61a1c137966b7667b400b72fa4a45c836257e443f3d77607db268/zstandard-0.25.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3b870ce5a02d4b22286cf4944c628e0f0881b11b3f14667c1d62185a99e04f53", size = 5054550, upload-time = "2025-09-14T22:18:26.445Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ce/26dc5a6fa956be41d0e984909224ed196ee6f91d607f0b3fd84577741a77/zstandard-0.25.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:05353cef599a7b0b98baca9b068dd36810c3ef0f42bf282583f438caf6ddcee3", size = 5401150, upload-time = "2025-09-14T22:18:28.745Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1b/402cab5edcfe867465daf869d5ac2a94930931c0989633bc01d6a7d8bd68/zstandard-0.25.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19796b39075201d51d5f5f790bf849221e58b48a39a5fc74837675d8bafc7362", size = 5448595, upload-time = "2025-09-14T22:18:30.475Z" }, + { url = "https://files.pythonhosted.org/packages/86/b2/fc50c58271a1ead0e5a0a0e6311f4b221f35954dce438ce62751b3af9b68/zstandard-0.25.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:53e08b2445a6bc241261fea89d065536f00a581f02535f8122eba42db9375530", size = 5555290, upload-time = "2025-09-14T22:18:32.336Z" }, + { url = "https://files.pythonhosted.org/packages/d2/20/5f72d6ba970690df90fdd37195c5caa992e70cb6f203f74cc2bcc0b8cf30/zstandard-0.25.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1f3689581a72eaba9131b1d9bdbfe520ccd169999219b41000ede2fca5c1bfdb", size = 5043898, upload-time = "2025-09-14T22:18:34.215Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f1/131a0382b8b8d11e84690574645f528f5c5b9343e06cefd77f5fd730cd2b/zstandard-0.25.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d8c56bb4e6c795fc77d74d8e8b80846e1fb8292fc0b5060cd8131d522974b751", size = 5571173, upload-time = "2025-09-14T22:18:36.117Z" }, + { url = "https://files.pythonhosted.org/packages/53/f6/2a37931023f737fd849c5c28def57442bbafadb626da60cf9ed58461fe24/zstandard-0.25.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:53f94448fe5b10ee75d246497168e5825135d54325458c4bfffbaafabcc0a577", size = 4958261, upload-time = "2025-09-14T22:18:38.098Z" }, + { url = "https://files.pythonhosted.org/packages/b5/52/ca76ed6dbfd8845a5563d3af4e972da3b9da8a9308ca6b56b0b929d93e23/zstandard-0.25.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c2ba942c94e0691467ab901fc51b6f2085ff48f2eea77b1a48240f011e8247c7", size = 5265680, upload-time = "2025-09-14T22:18:39.834Z" }, + { url = "https://files.pythonhosted.org/packages/7a/59/edd117dedb97a768578b49fb2f1156defb839d1aa5b06200a62be943667f/zstandard-0.25.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:07b527a69c1e1c8b5ab1ab14e2afe0675614a09182213f21a0717b62027b5936", size = 5439747, upload-time = "2025-09-14T22:18:41.647Z" }, + { url = "https://files.pythonhosted.org/packages/75/71/c2e9234643dcfbd6c5e975e9a2b0050e1b2afffda6c3a959e1b87997bc80/zstandard-0.25.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:51526324f1b23229001eb3735bc8c94f9c578b1bd9e867a0a646a3b17109f388", size = 5818805, upload-time = "2025-09-14T22:18:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/f5/93/8ebc19f0a31c44ea0e7348f9b0d4b326ed413b6575a3c6ff4ed50222abb6/zstandard-0.25.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89c4b48479a43f820b749df49cd7ba2dbc2b1b78560ecb5ab52985574fd40b27", size = 5362280, upload-time = "2025-09-14T22:18:45.625Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e9/29cc59d4a9d51b3fd8b477d858d0bd7ab627f700908bf1517f46ddd470ae/zstandard-0.25.0-cp39-cp39-win32.whl", hash = "sha256:1cd5da4d8e8ee0e88be976c294db744773459d51bb32f707a0f166e5ad5c8649", size = 436460, upload-time = "2025-09-14T22:18:49.077Z" }, + { url = "https://files.pythonhosted.org/packages/41/b5/bc7a92c116e2ef32dc8061c209d71e97ff6df37487d7d39adb51a343ee89/zstandard-0.25.0-cp39-cp39-win_amd64.whl", hash = "sha256:37daddd452c0ffb65da00620afb8e17abd4adaae6ce6310702841760c2c26860", size = 506097, upload-time = "2025-09-14T22:18:47.342Z" }, +] From c00b09c86c910c72e1a39f97da351f0d8b4f5e54 Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Wed, 8 Apr 2026 13:55:13 +0200 Subject: [PATCH 05/21] feat: update iceberg schema to snake_case and add entity_type --- crates/tdc-core/src/datacontainer/core.rs | 18 +- debug_warehouse/catalog.db | Bin 20480 -> 0 bytes ...-f000-4b09-aa96-69d2d17151c7.metadata.json | 1 - ...-1069-48a0-9f73-0f5337b4fa63.metadata.json | 1 - ...80fb8d-8d7d-435b-95e1-55f2b9ea7dd3-m0.avro | Bin 4474 -> 0 bytes ...-14bf-436f-9ed2-cbbfb2a553b8.metadata.json | 1 - ...-69bc-4280-800d-ab07ceb3c869.metadata.json | 1 - ...25fa0c-bd68-4ba7-bfc2-9a6c8f662a6d-m0.avro | Bin 5356 -> 0 bytes examples/08-idsse-warehouse.ipynb | 46 ++-- examples/idsse_warehouse/catalog.db | Bin 20480 -> 20480 bytes ...b82d-4b0c-9ae9-1f40d1c3d73a.metadata.json} | 2 +- ...9c65-44bb-baca-296106ac627b.metadata.json} | 2 +- ...b0b4-42c7-a2dc-f6178fd8f4ef.metadata.json} | 2 +- ...-1f7c-4788-b217-5191127f4512.metadata.json | 1 + ...-c7bf-4e2f-8e10-522378371fe8.metadata.json | 1 - ...0cfba-0ce6-4679-b7bb-4602f5f05406-m0.avro} | Bin 4490 -> 4491 bytes ...58ff54-e99b-4dae-b66a-ac94a48da697-m0.avro | Bin 4470 -> 4488 bytes ...6b00d-1cfa-47c8-b859-bc2936a263b0-m0.avro} | Bin 4489 -> 4489 bytes ...ae9c91-7f9c-4bec-b401-856370f90b74-m0.avro | Bin 4492 -> 0 bytes ...-b3defa57-62bd-4a60-87b5-0d6fcafd53ad.avro | Bin 1824 -> 0 bytes ...-cb26b00d-1cfa-47c8-b859-bc2936a263b0.avro | Bin 1819 -> 1821 bytes ...a858ff54-e99b-4dae-b66a-ac94a48da697.avro} | Bin 1900 -> 1893 bytes ...0bd0cfba-0ce6-4679-b7bb-4602f5f05406.avro} | Bin 1949 -> 1944 bytes ...3b69-41fb-a134-02dcfe960e12.metadata.json} | 2 +- ...6cd4-4964-a16c-803a68332e07.metadata.json} | 2 +- ...-7b87-44a6-acc5-96b3d3d6fd5d.metadata.json | 1 + ...-fa71-41cb-a109-30c7bf7cadf4.metadata.json | 1 - ...-5940-44b4-8fcd-39d9dc910155.metadata.json | 1 + ...-4237-409f-9d8b-49218b2e1ef4.metadata.json | 1 - ...4766b-2d66-44ac-b491-723a4772a1a3-m0.avro} | Bin 6365 -> 6357 bytes ...a5e91-e6e1-4926-9601-15797fb2ce84-m0.avro} | Bin 6347 -> 6351 bytes ...82c4c-10e1-429d-8391-918bcb86bb3f-m0.avro} | Bin 6329 -> 6335 bytes ...-d962db46-093c-45e8-be4a-0c6d572dad92.avro | Bin 1851 -> 0 bytes ...-0e34766b-2d66-44ac-b491-723a4772a1a3.avro | Bin 0 -> 1936 bytes ...-3faa5e91-e6e1-4926-9601-15797fb2ce84.avro | Bin 1829 -> 1852 bytes ...-4665052f-9f33-4c4a-b55d-e1fd2bcb223d.avro | Bin 2000 -> 0 bytes ...-7ad1e2c6-167d-47a8-a02b-391e6ef92b65.avro | Bin 1937 -> 0 bytes ...-78582c4c-10e1-429d-8391-918bcb86bb3f.avro | Bin 0 -> 1999 bytes profile_warehouse/catalog.db | Bin 20480 -> 0 bytes ...-65e0-450f-9be7-7684faeeac52.metadata.json | 1 - ...-89c7-436e-83d6-51ccb7009828.metadata.json | 1 - ...-e27f5ba5-48f7-4cb2-ae56-16a5fe7fd851.avro | Bin 1819 -> 0 bytes ...-7045-4467-ab11-81db388158b2.metadata.json | 1 - ...-eb36-4de8-8147-cabad48bca68.metadata.json | 1 - ...3266b5-4cd0-499d-b61d-580fce440e8d-m0.avro | Bin 5402 -> 0 bytes ...-f03266b5-4cd0-499d-b61d-580fce440e8d.avro | Bin 1829 -> 0 bytes python/tests/test_catalog.py | 5 +- python/tracking_data_container/catalog.py | 209 ++++++++++++++---- 48 files changed, 210 insertions(+), 92 deletions(-) delete mode 100644 debug_warehouse/catalog.db delete mode 100644 debug_warehouse/tracking/match_metadata/metadata/00000-140c93eb-f000-4b09-aa96-69d2d17151c7.metadata.json delete mode 100644 debug_warehouse/tracking/match_metadata/metadata/00001-702060e8-1069-48a0-9f73-0f5337b4fa63.metadata.json delete mode 100644 debug_warehouse/tracking/match_metadata/metadata/4f80fb8d-8d7d-435b-95e1-55f2b9ea7dd3-m0.avro delete mode 100644 debug_warehouse/tracking/tracking_data/metadata/00000-b6ef36f9-14bf-436f-9ed2-cbbfb2a553b8.metadata.json delete mode 100644 debug_warehouse/tracking/tracking_data/metadata/00001-32b80343-69bc-4280-800d-ab07ceb3c869.metadata.json delete mode 100644 debug_warehouse/tracking/tracking_data/metadata/e525fa0c-bd68-4ba7-bfc2-9a6c8f662a6d-m0.avro rename examples/idsse_warehouse/tracking/match_metadata/metadata/{00000-9af3a354-c746-4f8f-966b-040290fbf4fa.metadata.json => 00000-9ab50098-b82d-4b0c-9ae9-1f40d1c3d73a.metadata.json} (85%) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{00001-dc033d34-aa86-4054-844c-b33bbaf9b53b.metadata.json => 00001-638b3294-9c65-44bb-baca-296106ac627b.metadata.json} (61%) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{00002-2c7bc7d1-e21d-47a5-9d6f-6bc39ac8c361.metadata.json => 00002-e8eb68f3-b0b4-42c7-a2dc-f6178fd8f4ef.metadata.json} (57%) create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00003-03d4420e-1f7c-4788-b217-5191127f4512.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00003-16f8ad09-c7bf-4e2f-8e10-522378371fe8.metadata.json rename examples/idsse_warehouse/tracking/match_metadata/metadata/{b3defa57-62bd-4a60-87b5-0d6fcafd53ad-m0.avro => 0bd0cfba-0ce6-4679-b7bb-4602f5f05406-m0.avro} (92%) rename profile_warehouse/tracking/match_metadata/metadata/e27f5ba5-48f7-4cb2-ae56-16a5fe7fd851-m0.avro => examples/idsse_warehouse/tracking/match_metadata/metadata/a858ff54-e99b-4dae-b66a-ac94a48da697-m0.avro (92%) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{ed38ec79-f365-46e1-aa0a-6ff5e030781a-m0.avro => cb26b00d-1cfa-47c8-b859-bc2936a263b0-m0.avro} (92%) delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/faae9c91-7f9c-4bec-b401-856370f90b74-m0.avro delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/snap-1456969541151746380-0-b3defa57-62bd-4a60-87b5-0d6fcafd53ad.avro rename debug_warehouse/tracking/match_metadata/metadata/snap-8457677481870634211-0-4f80fb8d-8d7d-435b-95e1-55f2b9ea7dd3.avro => examples/idsse_warehouse/tracking/match_metadata/metadata/snap-3432642025755698322-0-cb26b00d-1cfa-47c8-b859-bc2936a263b0.avro (84%) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{snap-1114700784421547592-0-ed38ec79-f365-46e1-aa0a-6ff5e030781a.avro => snap-3611677702575916172-0-a858ff54-e99b-4dae-b66a-ac94a48da697.avro} (77%) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{snap-4866667276096503338-0-faae9c91-7f9c-4bec-b401-856370f90b74.avro => snap-6599460958304115199-0-0bd0cfba-0ce6-4679-b7bb-4602f5f05406.avro} (75%) rename examples/idsse_warehouse/tracking/tracking_data/metadata/{00000-c6360589-6439-4604-88e4-272cf35f75ce.metadata.json => 00000-7a3c0cd8-3b69-41fb-a134-02dcfe960e12.metadata.json} (90%) rename examples/idsse_warehouse/tracking/tracking_data/metadata/{00001-4bae733e-a73e-4da6-8812-af9a7d6b04e9.metadata.json => 00001-a4870a2b-6cd4-4964-a16c-803a68332e07.metadata.json} (57%) create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00002-8e9395a3-7b87-44a6-acc5-96b3d3d6fd5d.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00002-c4c023b8-fa71-41cb-a109-30c7bf7cadf4.metadata.json create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00003-34c7dd19-5940-44b4-8fcd-39d9dc910155.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00003-53fc18be-4237-409f-9d8b-49218b2e1ef4.metadata.json rename examples/idsse_warehouse/tracking/tracking_data/metadata/{d962db46-093c-45e8-be4a-0c6d572dad92-m0.avro => 0e34766b-2d66-44ac-b491-723a4772a1a3-m0.avro} (78%) rename examples/idsse_warehouse/tracking/tracking_data/metadata/{7ad1e2c6-167d-47a8-a02b-391e6ef92b65-m0.avro => 3faa5e91-e6e1-4926-9601-15797fb2ce84-m0.avro} (78%) rename examples/idsse_warehouse/tracking/tracking_data/metadata/{4665052f-9f33-4c4a-b55d-e1fd2bcb223d-m0.avro => 78582c4c-10e1-429d-8391-918bcb86bb3f-m0.avro} (78%) delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-377391038018914117-0-d962db46-093c-45e8-be4a-0c6d572dad92.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4114664439361220711-0-0e34766b-2d66-44ac-b491-723a4772a1a3.avro rename debug_warehouse/tracking/tracking_data/metadata/snap-6099500306663132840-0-e525fa0c-bd68-4ba7-bfc2-9a6c8f662a6d.avro => examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4345927569820710336-0-3faa5e91-e6e1-4926-9601-15797fb2ce84.avro (82%) delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4761525111313210917-0-4665052f-9f33-4c4a-b55d-e1fd2bcb223d.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-478866493446175361-0-7ad1e2c6-167d-47a8-a02b-391e6ef92b65.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-5344841726861284858-0-78582c4c-10e1-429d-8391-918bcb86bb3f.avro delete mode 100644 profile_warehouse/catalog.db delete mode 100644 profile_warehouse/tracking/match_metadata/metadata/00000-4a6aa9a1-65e0-450f-9be7-7684faeeac52.metadata.json delete mode 100644 profile_warehouse/tracking/match_metadata/metadata/00001-9b21cef5-89c7-436e-83d6-51ccb7009828.metadata.json delete mode 100644 profile_warehouse/tracking/match_metadata/metadata/snap-2219873109471251602-0-e27f5ba5-48f7-4cb2-ae56-16a5fe7fd851.avro delete mode 100644 profile_warehouse/tracking/tracking_data/metadata/00000-b3a6aabe-7045-4467-ab11-81db388158b2.metadata.json delete mode 100644 profile_warehouse/tracking/tracking_data/metadata/00001-6f0b9eff-eb36-4de8-8147-cabad48bca68.metadata.json delete mode 100644 profile_warehouse/tracking/tracking_data/metadata/f03266b5-4cd0-499d-b61d-580fce440e8d-m0.avro delete mode 100644 profile_warehouse/tracking/tracking_data/metadata/snap-3010651843225263066-0-f03266b5-4cd0-499d-b61d-580fce440e8d.avro diff --git a/crates/tdc-core/src/datacontainer/core.rs b/crates/tdc-core/src/datacontainer/core.rs index 45e166f..10c9970 100644 --- a/crates/tdc-core/src/datacontainer/core.rs +++ b/crates/tdc-core/src/datacontainer/core.rs @@ -395,8 +395,11 @@ impl TrackingDataContainer { let core_names: AHashSet<&str> = [ "match/id", + "match_id", "competition/id", + "competition_id", "season/id", + "season_id", "timestamp", "frame_id", "period", @@ -416,12 +419,17 @@ impl TrackingDataContainer { } else if name == "timestamp" { timestamp_idx = Some(i); core_indices.push(i); - } else if name == "player/id" { + } else if name == "player/id" || name == "player_id" { player_id_idx = Some(i); - } else if name == "team/id" { + attr_indices.push(i); // Also pivot it + } else if name == "team/id" || name == "team_id" { team_id_idx = Some(i); + attr_indices.push(i); // Also pivot it } else if core_names.contains(name) { core_indices.push(i); + } else if name == "entity_type" { + // Skip entity_type metadata during pivot + continue; } else { attr_indices.push(i); } @@ -447,11 +455,13 @@ impl TrackingDataContainer { let player_id_idx = player_id_idx.ok_or_else(|| { TrackingDataContainerErrorKind::InvalidParameters( - "Missing player/id column".to_string(), + "Missing player/id or player_id column".to_string(), ) })?; let team_id_idx = team_id_idx.ok_or_else(|| { - TrackingDataContainerErrorKind::InvalidParameters("Missing team/id column".to_string()) + TrackingDataContainerErrorKind::InvalidParameters( + "Missing team/id or team_id column".to_string(), + ) })?; // 3. Determine min/max frame index for fixed builder if possible diff --git a/debug_warehouse/catalog.db b/debug_warehouse/catalog.db deleted file mode 100644 index 36ea378b9bb8d1cf269b7325a0c2db34e631e5f1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20480 zcmeI&T~E_c7zglnybPAXywDIuOpPY6gu~9(y;U!C;u4JrIzh6l=CnJ7%2=W85N;Q) zjGqX<2^Yp2KLcKRwz*f`BqSJ!|7Och+ow6teouRGp6%V+72D^E<+!`dS5)bSB+JrM zMUf;aDBfe@-P!_TFxuJ?pK{lD&@d=1eY!an`5}#lPo?Q<~q+0J2b$gkv% z^c{Y5j;I4xuJU$VoKo65gTmh4K2 zAA0*`u~5`m)pr{E`Y;MTKJ4sRZCMV^M5FR?uBSAOopv#rdx1alH_x3Pyb<3ieH-ex z%5(w;$Lw;Sl|-HOvLhsIr_v5>xYrB3=kf#Fse1Z3VuzHblTG7}T~s;;M0p+u#%I=| ze=ASp60cK`=KhFyrY7po)ztL16bPS5;j`(_7kkcd6$Bsv0SG_<0uX=z1Rwx`D=ko~ z$P=@3^R>;|W{F#@TK0XHnLBo6tM#eZmw1+4=8s~r*oMblFD919hUg)1bKCb~&-no_ z*Bc12o-1_IsrbyUh~p)0RJZhx%;no+DahM#?LlLTSZj$G6N`2lCu+jTQZ=cPv}Twj znaEO-rL;ttK{KL-p_4WD~{&|v=7-?>)X-gyVq+yYy7!r+_62vqN%Sf

c5|Iv+YgAMh?rV&aL}byr7ICJM}Ju|G(pppXGxr< zHImFSN;E5@5^AMXHDe?#mR5Ve@#U0dSe8_jgPNMk4PuFVi6jlG5yrH%xR*+aQalq+ z#m!7_mLXA?K;%Sz{}fB*y_009U<00Izz00jO^fd^xvS0WE>lmzaJ znEA=Rn4W&sl(3T{-z4$E1_1~_00Izz00bZa0SG_<0uX?}H5Irgix$A`TYW76erS8X x=et#2|NUQ%e3!%v8w4N#0SG_<0uX=z1Rwwb2tWV=*ID3By$8_$H-Kgj;3q2ZSfKy_ diff --git a/debug_warehouse/tracking/match_metadata/metadata/00000-140c93eb-f000-4b09-aa96-69d2d17151c7.metadata.json b/debug_warehouse/tracking/match_metadata/metadata/00000-140c93eb-f000-4b09-aa96-69d2d17151c7.metadata.json deleted file mode 100644 index 55ab7ac..0000000 --- a/debug_warehouse/tracking/match_metadata/metadata/00000-140c93eb-f000-4b09-aa96-69d2d17151c7.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/debug_warehouse/tracking/match_metadata","table-uuid":"9b026c5b-217b-4c37-912c-4ccc1cd7a84f","last-updated-ms":1775575511744,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":true},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"snapshots":[],"snapshot-log":[],"metadata-log":[],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":0} diff --git a/debug_warehouse/tracking/match_metadata/metadata/00001-702060e8-1069-48a0-9f73-0f5337b4fa63.metadata.json b/debug_warehouse/tracking/match_metadata/metadata/00001-702060e8-1069-48a0-9f73-0f5337b4fa63.metadata.json deleted file mode 100644 index cbd84b5..0000000 --- a/debug_warehouse/tracking/match_metadata/metadata/00001-702060e8-1069-48a0-9f73-0f5337b4fa63.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/debug_warehouse/tracking/match_metadata","table-uuid":"9b026c5b-217b-4c37-912c-4ccc1cd7a84f","last-updated-ms":1775575511895,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":true},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"current-snapshot-id":8457677481870634211,"snapshots":[{"snapshot-id":8457677481870634211,"sequence-number":1,"timestamp-ms":1775575511895,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/debug_warehouse/tracking/match_metadata/metadata/snap-8457677481870634211-0-4f80fb8d-8d7d-435b-95e1-55f2b9ea7dd3.avro","summary":{"operation":"append","added-files-size":"1551","added-data-files":"1","added-records":"1","total-data-files":"1","total-delete-files":"0","total-records":"1","total-files-size":"1551","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0}],"snapshot-log":[{"snapshot-id":8457677481870634211,"timestamp-ms":1775575511895}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/debug_warehouse/tracking/match_metadata/metadata/00000-140c93eb-f000-4b09-aa96-69d2d17151c7.metadata.json","timestamp-ms":1775575511744}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":8457677481870634211,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":1} diff --git a/debug_warehouse/tracking/match_metadata/metadata/4f80fb8d-8d7d-435b-95e1-55f2b9ea7dd3-m0.avro b/debug_warehouse/tracking/match_metadata/metadata/4f80fb8d-8d7d-435b-95e1-55f2b9ea7dd3-m0.avro deleted file mode 100644 index 0c927794bb8e6faabad7e9dd3e1e951d386b27e7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4474 zcmb`K-)kII6vx{b(l(|E5-Ey7u0vusu-oj+Y<}#vHW5rPZLPsZ3=PY0cjj(0&FsuL zb0;B|AVQ@G5;TZHT9T@T_N^_L`XVHVf%c&f)|Ln&`WN^v_~5yBGP5(gX?}Gfgk)y! z_niAVKkm8j7B22@ZgSNLQo{e$UlQnyO@y4_QpZ!NkQ9nKF*H}mjb9RUO~{E!!Neu@ zMF~@Nq6?lSBWrZ?0{rig4?W!>@HwcE*`&Wz3YV%@$tF~%x@B%BP{f8CD#)RNOR&4k z#hGw%iBPOzij|9?vAz5Gq~PO8F#zc#a3Cht0gRL4bl^Dyl%H+0vEy$m<=RB8AI}T% zC-7xmx~*tA053H~92W@GNiX6l$2z838c~}yQZz6n&8lTmuyT<3d4#|BeV98Qga`p< z)pCG83WJ0oN`80{L=aQY4c~I-Q5rt>Ba8&yq{3_xMOK*Db|);Vu%K>;`pd-fvT?!m z41>KlEC_ZU{s5n`XHX%B*sHiCWCAf&qL^N(K%5F$d9SiX-5`p)rFLeo+B`FufeL4( zvfxfbuo1)xAyJO_#-qR(S}O1phQKcJ^8>L!<`9d05?PMQej!LF09p|@PQ%7@qS8=0 z8g8g)UY((>GnR^338MGk8%EPQoj^n95YI9a&kL^aJ5^;Nk{PUL#4YS;H(|w8+1hag zSW*UM;D~97Z6_c+5|k)nfkxD`4U+V?jZp!+L_>gtP7uc0FJ{DwIgZ3HJw>%VC?XIK z`T)dGOh?k%FOstLv%CzDRYbmmi*(-@V;ncZsk{0|MA1#9FhfaYEsN=B9`R}{8ydAJ zHW1T5OoN&6HDcwdT839L74}87dLO85^op?P%bAZI2hXsr>y(tB{XriI8cVq^1woh; z#a?Af6#GI}Y(=D5O%kpZD6rN9(ow2nZ_M*4YFym7GyeigohZ5AS*4$?J5E%;$TIZMb7O?M~b;o=VY2~mwQ3)&lw)&OM| zP)g|#(C$%YcaKs^*BT|Sog^ftYIqv-4SskaY-;E%J{xDtjumAqz+$A)Jv(4YWPqi; z1D3=+0G4V8nDIiK7vwPOP%(E*Y;Flt?yY6E%6$M+&h8zi+!ynHM};qEYYCH0F03Cw zUiwtSuI+b-I9|h=CXP}7?l2Vs$!J58x@(D6>S1}>8yeYlBanKwM!V^$BlXsfEoX%| zt2ji@wx15MrU#S6x~cpO-BjA9n{EnGu3_ehX$* z+aWHy^ujUif(f{afC+X{fk|0+xhwui6-;~%4Bb37!GjVu^&)Yp0y55wFuU)iw(EzR z51&4ze)r8n;{Hgw?x*L}LhjzbfB&=chxzBnYxh@I?_B9j$w!}@Qk4E5`j&5vtz1v^ zcXlo(y0hQk82hVZIoW-51ueh5aw8#TkIP+eob0&UE}qQx-)mc-)x!sx=Q%Hr5p z3tuYduQh&fbLr~X{IwUBuAZMi_~_e~*P8|g)(6(xAO4I|4}`&WVeQ1=`Y*p{hSwiu zo{X$N&OA&!kh*_8(Xlpm8{NHiC$oIHYgJ67A9r8Y7wcOO{FWQ~y76A)+@a5#2AW%+ rZ++#9I`QSzGYt*s%BP>T6n=c|z-celu=L5H8*}^jy?NwJ<%a(Uw&;i< diff --git a/debug_warehouse/tracking/tracking_data/metadata/00000-b6ef36f9-14bf-436f-9ed2-cbbfb2a553b8.metadata.json b/debug_warehouse/tracking/tracking_data/metadata/00000-b6ef36f9-14bf-436f-9ed2-cbbfb2a553b8.metadata.json deleted file mode 100644 index 9396968..0000000 --- a/debug_warehouse/tracking/tracking_data/metadata/00000-b6ef36f9-14bf-436f-9ed2-cbbfb2a553b8.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/debug_warehouse/tracking/tracking_data","table-uuid":"5f22a43f-a3e8-4c88-8ce9-5abed223a490","last-updated-ms":1775575511736,"last-column-id":11,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[{"source-id":1,"field-id":1000,"transform":"identity","name":"match/id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"snapshots":[],"snapshot-log":[],"metadata-log":[],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":0} diff --git a/debug_warehouse/tracking/tracking_data/metadata/00001-32b80343-69bc-4280-800d-ab07ceb3c869.metadata.json b/debug_warehouse/tracking/tracking_data/metadata/00001-32b80343-69bc-4280-800d-ab07ceb3c869.metadata.json deleted file mode 100644 index cb0921d..0000000 --- a/debug_warehouse/tracking/tracking_data/metadata/00001-32b80343-69bc-4280-800d-ab07ceb3c869.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/debug_warehouse/tracking/tracking_data","table-uuid":"5f22a43f-a3e8-4c88-8ce9-5abed223a490","last-updated-ms":1775575511888,"last-column-id":14,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false},{"id":12,"name":"player_id","type":"string","required":false},{"id":13,"name":"team_id","type":"string","required":false},{"id":14,"name":"entity_key","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":1,"field-id":1000,"transform":"identity","name":"match/id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":6099500306663132840,"snapshots":[{"snapshot-id":6099500306663132840,"sequence-number":1,"timestamp-ms":1775575511888,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/debug_warehouse/tracking/tracking_data/metadata/snap-6099500306663132840-0-e525fa0c-bd68-4ba7-bfc2-9a6c8f662a6d.avro","summary":{"operation":"append","added-files-size":"4767","added-data-files":"1","added-records":"2","changed-partition-count":"1","total-data-files":"1","total-delete-files":"0","total-records":"2","total-files-size":"4767","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":6099500306663132840,"timestamp-ms":1775575511888}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/debug_warehouse/tracking/tracking_data/metadata/00000-b6ef36f9-14bf-436f-9ed2-cbbfb2a553b8.metadata.json","timestamp-ms":1775575511736}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":6099500306663132840,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":1} diff --git a/debug_warehouse/tracking/tracking_data/metadata/e525fa0c-bd68-4ba7-bfc2-9a6c8f662a6d-m0.avro b/debug_warehouse/tracking/tracking_data/metadata/e525fa0c-bd68-4ba7-bfc2-9a6c8f662a6d-m0.avro deleted file mode 100644 index c5d44b17b6be17c165f16c0eb14a5fb1267b6de4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5356 zcmb_gYitx%6o#UaASsG1@j>X#V2qG$*`4iMED^L?lu}EH)Mj08XYbOn^J3<1+bvxa zsv$sxm{71pF+xR&;fJq~G$@KcM%`M9V^q=>;)Hm&8v2v-^KaSE2% z8#1Os%K~`jOR^D_)$qSg*2F}e!0S;g86@GeY6bY6Q5_%%Cm&r>$2xeaW2q5cR5O6A%(awooah;$Rqhy5g5``% zD;(1f7o(`8Vn&Hk@Co=GqxkrZTtan@-^gW>X2RmB1yFZP8U|bzFiP;*~t40m@W|8)P5hD z(eDS$sg4yRs_HT{rRA2glv3L?5#qeA!C#JlHZ#Puu#M;jbYl*mhud_uNKhk0$c>Pw zgel1tRE0viQzMw-S?1@~VQwz8^#TDY1XTxSBS$e1WNsyaKu$1?8P2EXQIH>Aw&VtJ zqC&ZZ4<%VBSTkBwYL;i%sbv;$&6+qRCP~b#*?=MV19HYOiE@>QedKisvP2Ytgq4`w zO!Sm&f)jNc6(tfj#`Il0QQr*lFbj~9EblrTZwZErnBpi9Ov=ry0>e-h06(r8;v$P5 zh!wIDvD*8PstfEF0y+VpDRnc^&@gRD4Fptm%WXG`6YCjOz-%0#mGw2KU8F6jdId6B zM!dW*{aZ<;MMz=>8(^jiI1wgv8mD8@Vojg|=EMwl@YQT`cqz>q*+Iu;_3$AWuSJ0X zXIBrah22-~5Kd!0GqPXP1Ad;tXjit*))>SHhGqh@T72!c}37Q$+5sS#po ze&BwE2(ZM`APY8zrFOj{+~9H%5z zLjwL}{#s+$Ic9@Ov4ofp#C)(YmJg3C2&xp5l`wlF-FrEtJ*-hqS=^4px{g6>p*obv zpx>ZhwxGFsXcwV15GV_`1p?&`R?H$vB+XIcgtMD58P!0UC-XKe#Y|bWd%UV`)>itip-(p{n>`n>>VEvQ;=d2&?|FjA*@7DEw1%q%f?xFEP!GE z$|AsgfC0=;9AG{$0f71J0nG41CSJf{rcj=A6s(L1X0R-qUsrvL9FD z2P?7(Cfk^p9sn;bR2|i}6v7Lyq_z`1+zhzGS_?=f3ld+FO=$U2#?vw;K43t~)z>*8o2W_2*}OihEgpr-u)sOg9x2D2!qCRQVX2tze26N=fQ ztHA`x9AUh8a;o;(f;f*+4w9+#iXRD@U zSejXe)$A)K*f0zfW1A=J84*H>E{h65y4sHH4B7tX{2hUq6s{+*+!;kFpsPAyu1BDG z(l_nmk*1`%YmHU$n#W;R)pTO8TRV(t146(R2tu$65^Tz%VWwiORQATF!_sZm#(p4U zMU28%PhoJQ$GOC$g}=96IN&S$taqERp;rBCbI$hL<;`C#{kkr1;QP;KFMWOHlBs)o z_H7B241GH7bipr&YQ8ys>Y;wITdd#O5G=nsuWQP#gNyGt|0mbIwJ)!0a>1bopW1c$ zzN)I@mv0>3bv1GAqmIOl&Mt1!hwr>_*OgG{*BkSe*C%=o@Bi+eOV=AeDEOyo&cNKa z&ozGa)&+F`s-mO4XAVC*(7ku)z;APA?0aOpwrI}#T|1fz&uyFDc=q}O1L7Z-Rt}!{ z>ExLfd2{<~TaKLA5YKmw?qjX}e=i%n(pglyvFE6<`&i9fwd(c3p1lih+jK8CSodYgzP=4Jzuo)d zlsAVK=RZ9?fBm#2>mFN|v*gZ(Nz)phugZV-mA=@+Pv%Lp)_+_#w6R{?+Sb%vFr$C| j8|SJv%{_QxXV=Y(wb!;S{r6(do-?Nk-dnRK72W>;u6fjc diff --git a/examples/08-idsse-warehouse.ipynb b/examples/08-idsse-warehouse.ipynb index 477b6a2..044506e 100644 --- a/examples/08-idsse-warehouse.ipynb +++ b/examples/08-idsse-warehouse.ipynb @@ -138,15 +138,15 @@ "text": [ "\n", "Processing match DFL-MAT-J03WMX...\n", - " ✅ Parsed in 1.76s, Saved in 2.21s\n", + " ✅ Parsed in 1.52s, Saved in 1.92s\n", " ✅ Rows: 145,967\n", "\n", "Processing match DFL-MAT-J03WN1...\n", - " ✅ Parsed in 1.91s, Saved in 1.93s\n", + " ✅ Parsed in 1.27s, Saved in 1.54s\n", " ✅ Rows: 141,561\n", "\n", "Processing match DFL-MAT-J03WOH...\n", - " ✅ Parsed in 1.30s, Saved in 1.73s\n", + " ✅ Parsed in 1.18s, Saved in 1.43s\n", " ✅ Rows: 137,214\n" ] } @@ -242,7 +242,7 @@ "output_type": "stream", "text": [ "Loading data for DFL-MAT-J03WMX...\n", - " ✅ Loaded and pivoted 165259 rows in 1.3019s\n" + " ✅ Loaded and pivoted 165259 rows in 1.1780s\n" ] }, { @@ -255,33 +255,33 @@ " white-space: pre-wrap;\n", "}\n", "\n", - "shape: (5, 348)
match/idcompetition/idseason/idtimestampframe_idperiodteams/home/players/DFL-OBJ-0002HE/xteams/home/players/DFL-OBJ-0002HE/yteams/home/players/DFL-OBJ-0002HE/zteams/home/players/DFL-OBJ-0002HE/velteams/home/players/DFL-OBJ-0002HE/accteams/home/players/DFL-OBJ-0002HE/entity_keyteams/home/players/DFL-OBJ-0002HE/idteams/home/players/DFL-OBJ-0002HE/jersey_colourteams/home/players/DFL-OBJ-0002HE/nameteams/home/xteams/home/yteams/home/zteams/home/velteams/home/accteams/home/entity_keyteams/home/idteams/home/jersey_colourteams/home/nameteams/away/players/DFL-OBJ-0002F5/xteams/away/players/DFL-OBJ-0002F5/yteams/away/players/DFL-OBJ-0002F5/zteams/away/players/DFL-OBJ-0002F5/velteams/away/players/DFL-OBJ-0002F5/accteams/away/players/DFL-OBJ-0002F5/entity_keyteams/away/players/DFL-OBJ-0002F5/idteams/away/players/DFL-OBJ-0002F5/jersey_colourteams/away/players/DFL-OBJ-0002F5/nameteams/away/players/DFL-OBJ-0002AU/xteams/away/players/DFL-OBJ-0002AU/yteams/away/players/DFL-OBJ-0002AU/zteams/away/players/DFL-OBJ-0002AU/velteams/away/nameteams/away/players/DFL-OBJ-J017RE/xteams/away/players/DFL-OBJ-J017RE/yteams/away/players/DFL-OBJ-J017RE/zteams/away/players/DFL-OBJ-J017RE/velteams/away/players/DFL-OBJ-J017RE/accteams/away/players/DFL-OBJ-J017RE/entity_keyteams/away/players/DFL-OBJ-J017RE/idteams/away/players/DFL-OBJ-J017RE/jersey_colourteams/away/players/DFL-OBJ-J017RE/nameteams/away/players/DFL-OBJ-000191/xteams/away/players/DFL-OBJ-000191/yteams/away/players/DFL-OBJ-000191/zteams/away/players/DFL-OBJ-000191/velteams/away/players/DFL-OBJ-000191/accteams/away/players/DFL-OBJ-000191/entity_keyteams/away/players/DFL-OBJ-000191/idteams/away/players/DFL-OBJ-000191/jersey_colourteams/away/players/DFL-OBJ-000191/nameteams/away/players/DFL-OBJ-0027KL/xteams/away/players/DFL-OBJ-0027KL/yteams/away/players/DFL-OBJ-0027KL/zteams/away/players/DFL-OBJ-0027KL/velteams/away/players/DFL-OBJ-0027KL/accteams/away/players/DFL-OBJ-0027KL/entity_keyteams/away/players/DFL-OBJ-0027KL/idteams/away/players/DFL-OBJ-0027KL/jersey_colourteams/away/players/DFL-OBJ-0027KL/nameteams/away/players/DFL-OBJ-0000IA/xteams/away/players/DFL-OBJ-0000IA/yteams/away/players/DFL-OBJ-0000IA/zteams/away/players/DFL-OBJ-0000IA/velteams/away/players/DFL-OBJ-0000IA/accteams/away/players/DFL-OBJ-0000IA/entity_keyteams/away/players/DFL-OBJ-0000IA/idteams/away/players/DFL-OBJ-0000IA/jersey_colourteams/away/players/DFL-OBJ-0000IA/name
strstrstrdatetime[μs]i64strf32f32f32f64f64strstrstrstrf32f32f32f64f64strstrstrstrf32f32f32f64f64strstrstrstrf32f32f32f64strf32f32f32f64f64strstrstrstrf32f32f32f64f64strstrstrstrf32f32f32f64f64strstrstrstrf32f32f32f64f64strstrstrstr
"DFL-MAT-J03WMX"nullnull2023-05-27 13:30:12.60010000"first_half"47.3699990.25null0.00.0"DFL-OBJ-0002HE|home"nullnullnullnullnullnullnullnull"team:home|home""DFL-CLU-000008""#F4F5F0""1. FC Köln"-9.09-1.96null0.00.0"DFL-OBJ-0002F5|away"nullnullnull0.11-21.870001null0.0"FC Bayern München"-20.779.07null0.00.0"DFL-OBJ-J017RE|away"nullnullnullnullnullnullnullnullnullnullnullnull-16.0499991.04null0.00.0"DFL-OBJ-0027KL|away"nullnullnull-1.5312.68null0.00.0"DFL-OBJ-0000IA|away"nullnullnull
"DFL-MAT-J03WMX"nullnull2023-05-27 13:30:12.64010001"first_half"47.340.26null3.560.42"DFL-OBJ-0002HE|home"nullnullnullnullnullnullnullnull"team:home|home""DFL-CLU-000008""#F4F5F0""1. FC Köln"-9.06-1.95null3.10.0"DFL-OBJ-0002F5|away"nullnullnull0.08-21.870001null1.73"FC Bayern München"-20.779.11null3.12.11"DFL-OBJ-J017RE|away"nullnullnullnullnullnullnullnullnullnullnullnull-15.980.99null7.270.66"DFL-OBJ-0027KL|away"nullnullnull-1.4512.68null11.340.0"DFL-OBJ-0000IA|away"nullnullnull
"DFL-MAT-J03WMX"nullnull2023-05-27 13:30:12.68010002"first_half"47.2999990.26null3.620.37"DFL-OBJ-0002HE|home"nullnullnullnullnullnullnullnull"team:home|home""DFL-CLU-000008""#F4F5F0""1. FC Köln"-9.02-1.94null3.170.0"DFL-OBJ-0002F5|away"nullnullnull0.04-21.860001null2.03"FC Bayern München"-20.779.14null3.42.06"DFL-OBJ-J017RE|away"nullnullnullnullnullnullnullnullnullnullnullnull-15.920.94null7.32-0.04"DFL-OBJ-0027KL|away"nullnullnull-1.2712.65null12.80.0"DFL-OBJ-0000IA|away"nullnullnull
"DFL-MAT-J03WMX"nullnull2023-05-27 13:30:12.72010003"first_half"47.2599980.26null3.680.34"DFL-OBJ-0002HE|home"nullnullnullnullnullnullnullnull"team:home|home""DFL-CLU-000008""#F4F5F0""1. FC Köln"-8.99-1.93null3.290.0"DFL-OBJ-0002F5|away"nullnullnull0.01-21.85null2.57"FC Bayern München"-20.779.19null3.72.0"DFL-OBJ-J017RE|away"nullnullnullnullnullnullnullnullnullnullnullnull-15.860.89null7.26-0.74"DFL-OBJ-0027KL|away"nullnullnull-1.0712.64null14.260.0"DFL-OBJ-0000IA|away"nullnullnull
"DFL-MAT-J03WMX"nullnull2023-05-27 13:30:12.76010004"first_half"47.2200010.27null3.730.32"DFL-OBJ-0002HE|home"nullnullnullnullnullnullnullnull"team:home|home""DFL-CLU-000008""#F4F5F0""1. FC Köln"-8.96-1.91null3.440.0"DFL-OBJ-0002F5|away"nullnullnull-0.01-21.85null1.87"FC Bayern München"-20.7800019.22null3.981.92"DFL-OBJ-J017RE|away"nullnullnullnullnullnullnullnullnullnullnullnull-15.80.84null7.1-1.4"DFL-OBJ-0027KL|away"nullnullnull-0.8812.61null15.70.0"DFL-OBJ-0000IA|away"nullnullnull
" + "shape: (5, 348)
match/idcompetition/idseason/idtimestampframe_idperiodteams/away/players/DFL-OBJ-0027G6/xteams/away/players/DFL-OBJ-0027G6/yteams/away/players/DFL-OBJ-0027G6/zteams/away/players/DFL-OBJ-0027G6/accteams/away/players/DFL-OBJ-0027G6/velteams/away/players/DFL-OBJ-0027G6/entity_keyteams/away/players/DFL-OBJ-0027G6/idteams/away/players/DFL-OBJ-0027G6/jersey_colourteams/away/players/DFL-OBJ-0027G6/namereferees/DFL-OBJ-000141/xreferees/DFL-OBJ-000141/yreferees/DFL-OBJ-000141/zreferees/DFL-OBJ-000141/accreferees/DFL-OBJ-000141/velreferees/DFL-OBJ-000141/entity_keyreferees/DFL-OBJ-000141/idreferees/DFL-OBJ-000141/jersey_colourreferees/DFL-OBJ-000141/nameteams/home/players/DFL-OBJ-0027B6/xteams/home/players/DFL-OBJ-0027B6/yteams/home/players/DFL-OBJ-0027B6/zteams/home/players/DFL-OBJ-0027B6/accteams/home/players/DFL-OBJ-0027B6/velteams/home/players/DFL-OBJ-0027B6/entity_keyteams/home/players/DFL-OBJ-0027B6/idteams/home/players/DFL-OBJ-0027B6/jersey_colourteams/home/players/DFL-OBJ-0027B6/nameteams/away/players/DFL-OBJ-0000RP/xteams/away/players/DFL-OBJ-0000RP/yteams/away/players/DFL-OBJ-0000RP/zteams/away/players/DFL-OBJ-0000RP/accteams/away/players/DFL-OBJ-0002DR/nameteams/away/players/DFL-OBJ-0027KL/xteams/away/players/DFL-OBJ-0027KL/yteams/away/players/DFL-OBJ-0027KL/zteams/away/players/DFL-OBJ-0027KL/accteams/away/players/DFL-OBJ-0027KL/velteams/away/players/DFL-OBJ-0027KL/entity_keyteams/away/players/DFL-OBJ-0027KL/idteams/away/players/DFL-OBJ-0027KL/jersey_colourteams/away/players/DFL-OBJ-0027KL/nameteams/home/players/DFL-OBJ-0002BO/xteams/home/players/DFL-OBJ-0002BO/yteams/home/players/DFL-OBJ-0002BO/zteams/home/players/DFL-OBJ-0002BO/accteams/home/players/DFL-OBJ-0002BO/velteams/home/players/DFL-OBJ-0002BO/entity_keyteams/home/players/DFL-OBJ-0002BO/idteams/home/players/DFL-OBJ-0002BO/jersey_colourteams/home/players/DFL-OBJ-0002BO/nameteams/home/players/DFL-OBJ-00012X/xteams/home/players/DFL-OBJ-00012X/yteams/home/players/DFL-OBJ-00012X/zteams/home/players/DFL-OBJ-00012X/accteams/home/players/DFL-OBJ-00012X/velteams/home/players/DFL-OBJ-00012X/entity_keyteams/home/players/DFL-OBJ-00012X/idteams/home/players/DFL-OBJ-00012X/jersey_colourteams/home/players/DFL-OBJ-00012X/nameteams/home/players/DFL-OBJ-0027V2/xteams/home/players/DFL-OBJ-0027V2/yteams/home/players/DFL-OBJ-0027V2/zteams/home/players/DFL-OBJ-0027V2/accteams/home/players/DFL-OBJ-0027V2/velteams/home/players/DFL-OBJ-0027V2/entity_keyteams/home/players/DFL-OBJ-0027V2/idteams/home/players/DFL-OBJ-0027V2/jersey_colourteams/home/players/DFL-OBJ-0027V2/name
strstrstrdatetime[μs]i64strf32f32f32f64f64strstrstrstrf32f32f32f64f64strstrstrstrf32f32f32f64f64strstrstrstrf32f32f32f64strf32f32f32f64f64strstrstrstrf32f32f32f64f64strstrstrstrf32f32f32f64f64strstrstrstrf32f32f32f64f64strstrstrstr
"DFL-MAT-J03WMX"nullnull2023-05-27 13:30:12.60010000"first_half"0.78-0.54null0.00.0"DFL-OBJ-0027G6|away"nullnullnull1.218.15null0.00.0"referee:DFL-OBJ-000141|none"nullnullnull20.167.08null0.00.0"DFL-OBJ-0027B6|home"nullnullnullnullnullnullnullnull-16.0499991.04null0.00.0"DFL-OBJ-0027KL|away"nullnullnull17.2620.85null0.00.0"DFL-OBJ-0002BO|home"nullnullnull13.08-13.71null0.00.0"DFL-OBJ-00012X|home"nullnullnullnullnullnullnullnullnullnullnullnull
"DFL-MAT-J03WMX"nullnull2023-05-27 13:30:12.64010001"first_half"0.85-0.56null2.347.74"DFL-OBJ-0027G6|away"nullnullnull1.2818.129999null0.676.96"referee:DFL-OBJ-000141|none"nullnullnull20.1200017.09null0.742.98"DFL-OBJ-0027B6|home"nullnullnullnullnullnullnullnull-15.980.99null0.667.27"DFL-OBJ-0027KL|away"nullnullnull17.2320.879999null1.084.43"DFL-OBJ-0002BO|home"nullnullnull13.06-13.75null1.43.49"DFL-OBJ-00012X|home"nullnullnullnullnullnullnullnullnullnullnullnull
"DFL-MAT-J03WMX"nullnull2023-05-27 13:30:12.68010002"first_half"0.94-0.59null2.288.08"DFL-OBJ-0027G6|away"nullnullnull1.3418.09null0.617.05"referee:DFL-OBJ-000141|none"nullnullnull20.097.1null0.653.09"DFL-OBJ-0027B6|home"nullnullnullnullnullnullnullnull-15.920.94null-0.047.32"DFL-OBJ-0027KL|away"nullnullnull17.1820.91null1.074.6"DFL-OBJ-0002BO|home"nullnullnull13.05-13.78null1.343.73"DFL-OBJ-00012X|home"nullnullnullnullnullnullnullnullnullnullnullnull
"DFL-MAT-J03WMX"nullnull2023-05-27 13:30:12.72010003"first_half"1.03-0.62null2.268.4"DFL-OBJ-0027G6|away"nullnullnull1.4118.059999null0.557.14"referee:DFL-OBJ-000141|none"nullnullnull20.0599997.12null0.563.18"DFL-OBJ-0027B6|home"nullnullnullnullnullnullnullnull-15.860.89null-0.747.26"DFL-OBJ-0027KL|away"nullnullnull17.1520.940001null1.034.74"DFL-OBJ-0002BO|home"nullnullnull13.03-13.82null1.243.95"DFL-OBJ-00012X|home"nullnullnullnullnullnullnullnullnullnullnullnull
"DFL-MAT-J03WMX"nullnull2023-05-27 13:30:12.76010004"first_half"1.12-0.65null2.298.74"DFL-OBJ-0027G6|away"nullnullnull1.4918.02null0.477.2"referee:DFL-OBJ-000141|none"nullnullnull20.027.13null0.443.25"DFL-OBJ-0027B6|home"nullnullnullnullnullnullnullnull-15.80.84null-1.47.1"DFL-OBJ-0027KL|away"nullnullnull17.11000120.98null0.984.88"DFL-OBJ-0002BO|home"nullnullnull13.02-13.87null1.114.13"DFL-OBJ-00012X|home"nullnullnullnullnullnullnullnullnullnullnullnull
" ], "text/plain": [ "shape: (5, 348)\n", "┌───────────┬───────────┬───────────┬───────────┬───┬───────────┬───────────┬───────────┬──────────┐\n", - "│ match/id ┆ competiti ┆ season/id ┆ timestamp ┆ … ┆ teams/awa ┆ teams/awa ┆ teams/awa ┆ teams/aw │\n", - "│ --- ┆ on/id ┆ --- ┆ --- ┆ ┆ y/players ┆ y/players ┆ y/players ┆ ay/playe │\n", + "│ match/id ┆ competiti ┆ season/id ┆ timestamp ┆ … ┆ teams/hom ┆ teams/hom ┆ teams/hom ┆ teams/ho │\n", + "│ --- ┆ on/id ┆ --- ┆ --- ┆ ┆ e/players ┆ e/players ┆ e/players ┆ me/playe │\n", "│ str ┆ --- ┆ str ┆ datetime[ ┆ ┆ /DFL-OBJ- ┆ /DFL-OBJ- ┆ /DFL-OBJ- ┆ rs/DFL-O │\n", - "│ ┆ str ┆ ┆ μs] ┆ ┆ 000… ┆ 000… ┆ 000… ┆ BJ-000… │\n", + "│ ┆ str ┆ ┆ μs] ┆ ┆ 002… ┆ 002… ┆ 002… ┆ BJ-002… │\n", "│ ┆ ┆ ┆ ┆ ┆ --- ┆ --- ┆ --- ┆ --- │\n", "│ ┆ ┆ ┆ ┆ ┆ str ┆ str ┆ str ┆ str │\n", "╞═══════════╪═══════════╪═══════════╪═══════════╪═══╪═══════════╪═══════════╪═══════════╪══════════╡\n", - "│ DFL-MAT-J ┆ null ┆ null ┆ 2023-05-2 ┆ … ┆ DFL-OBJ-0 ┆ null ┆ null ┆ null │\n", - "│ 03WMX ┆ ┆ ┆ 7 13:30:1 ┆ ┆ 000IA|awa ┆ ┆ ┆ │\n", - "│ ┆ ┆ ┆ 2.600 ┆ ┆ y ┆ ┆ ┆ │\n", - "│ DFL-MAT-J ┆ null ┆ null ┆ 2023-05-2 ┆ … ┆ DFL-OBJ-0 ┆ null ┆ null ┆ null │\n", - "│ 03WMX ┆ ┆ ┆ 7 13:30:1 ┆ ┆ 000IA|awa ┆ ┆ ┆ │\n", - "│ ┆ ┆ ┆ 2.640 ┆ ┆ y ┆ ┆ ┆ │\n", - "│ DFL-MAT-J ┆ null ┆ null ┆ 2023-05-2 ┆ … ┆ DFL-OBJ-0 ┆ null ┆ null ┆ null │\n", - "│ 03WMX ┆ ┆ ┆ 7 13:30:1 ┆ ┆ 000IA|awa ┆ ┆ ┆ │\n", - "│ ┆ ┆ ┆ 2.680 ┆ ┆ y ┆ ┆ ┆ │\n", - "│ DFL-MAT-J ┆ null ┆ null ┆ 2023-05-2 ┆ … ┆ DFL-OBJ-0 ┆ null ┆ null ┆ null │\n", - "│ 03WMX ┆ ┆ ┆ 7 13:30:1 ┆ ┆ 000IA|awa ┆ ┆ ┆ │\n", - "│ ┆ ┆ ┆ 2.720 ┆ ┆ y ┆ ┆ ┆ │\n", - "│ DFL-MAT-J ┆ null ┆ null ┆ 2023-05-2 ┆ … ┆ DFL-OBJ-0 ┆ null ┆ null ┆ null │\n", - "│ 03WMX ┆ ┆ ┆ 7 13:30:1 ┆ ┆ 000IA|awa ┆ ┆ ┆ │\n", - "│ ┆ ┆ ┆ 2.760 ┆ ┆ y ┆ ┆ ┆ │\n", + "│ DFL-MAT-J ┆ null ┆ null ┆ 2023-05-2 ┆ … ┆ null ┆ null ┆ null ┆ null │\n", + "│ 03WMX ┆ ┆ ┆ 7 13:30:1 ┆ ┆ ┆ ┆ ┆ │\n", + "│ ┆ ┆ ┆ 2.600 ┆ ┆ ┆ ┆ ┆ │\n", + "│ DFL-MAT-J ┆ null ┆ null ┆ 2023-05-2 ┆ … ┆ null ┆ null ┆ null ┆ null │\n", + "│ 03WMX ┆ ┆ ┆ 7 13:30:1 ┆ ┆ ┆ ┆ ┆ │\n", + "│ ┆ ┆ ┆ 2.640 ┆ ┆ ┆ ┆ ┆ │\n", + "│ DFL-MAT-J ┆ null ┆ null ┆ 2023-05-2 ┆ … ┆ null ┆ null ┆ null ┆ null │\n", + "│ 03WMX ┆ ┆ ┆ 7 13:30:1 ┆ ┆ ┆ ┆ ┆ │\n", + "│ ┆ ┆ ┆ 2.680 ┆ ┆ ┆ ┆ ┆ │\n", + "│ DFL-MAT-J ┆ null ┆ null ┆ 2023-05-2 ┆ … ┆ null ┆ null ┆ null ┆ null │\n", + "│ 03WMX ┆ ┆ ┆ 7 13:30:1 ┆ ┆ ┆ ┆ ┆ │\n", + "│ ┆ ┆ ┆ 2.720 ┆ ┆ ┆ ┆ ┆ │\n", + "│ DFL-MAT-J ┆ null ┆ null ┆ 2023-05-2 ┆ … ┆ null ┆ null ┆ null ┆ null │\n", + "│ 03WMX ┆ ┆ ┆ 7 13:30:1 ┆ ┆ ┆ ┆ ┆ │\n", + "│ ┆ ┆ ┆ 2.760 ┆ ┆ ┆ ┆ ┆ │\n", "└───────────┴───────────┴───────────┴───────────┴───┴───────────┴───────────┴───────────┴──────────┘" ] }, diff --git a/examples/idsse_warehouse/catalog.db b/examples/idsse_warehouse/catalog.db index de0809ea32fad02b8cfb3182ee67b1bed7a67ceb..db8fc534bd9ac8e0e7090cf4bbef13815e09e9ea 100644 GIT binary patch delta 178 zcmWN~u@QqX3_#IifouRYw7Row*^)NM8rZVBw5h4n(xqYq?lMgF$ONJP_x^Sa+cBKa zaY@wJ3>C6c0HR7D-VBnOT2l;|^Zp)Ri*!wz*92UJuW&7}ww6IlO_r>@K##!Gzc=R7T4dM)A!YPil68^ao{ffrLLSWPi1$~C#HC0Pm+7NH^$6+)n yZ7hEd+{r*XdRS=`BqX#h zm_KKJ_@o&#*+u-ePFl>p1VL>&cRN zm0g%kVF7oGrzYFUOwZ0;Y$sQy?NUp7Hf@*ZGfz*4X^nxiGG{fe>};GF=a>|jn3=^H z-;f-Tup*n|vM{F_-!kbQ9nl<78DR;@1{pOmH8Ta)B%_9zJUkA3+>?8{XZNpLpWLx- ddSl^^{p{@CrMh~FY-$tq*hCmCJ_MnM0RZ0@c*_6) delta 283 zcmeBH?o!@RDNxVc*qHK+FOPrrKh5cdNx}P>?l6~j+xi_gkZ8N>A2_vhW+U?kiRhLk z9czOU9YUC{aBX|B`ERSs6aL$+9vzL1a|QVh$Xd8YS~9RJzC3WudQbf&<`3UKZmp}U z{C_94XRV^Pmz9u{j%=iQ)NKvdX(jLG&bVkh)z>?$qWjmj75PE+&2qtaugG=p`oHq< zD&3Qvhb-J*GDn@7J$13YL&%HLd0|fq9gZG*qM*04izijWzhcvR9{U3>pAL6+?KJUR zW@ud4lD6Kpdb`s8QoFr-BINWRq)R*fkYl~OgFD`Rjl95t9sLV8`KGRsXMA(U@7#}? dT@RKU?V1(ee(>?-dh_PSr2WDtE(D?n0RWWAhIaq} diff --git a/profile_warehouse/tracking/match_metadata/metadata/e27f5ba5-48f7-4cb2-ae56-16a5fe7fd851-m0.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/a858ff54-e99b-4dae-b66a-ac94a48da697-m0.avro similarity index 92% rename from profile_warehouse/tracking/match_metadata/metadata/e27f5ba5-48f7-4cb2-ae56-16a5fe7fd851-m0.avro rename to examples/idsse_warehouse/tracking/match_metadata/metadata/a858ff54-e99b-4dae-b66a-ac94a48da697-m0.avro index 2b7d9b67e3ec2fbc252e031cebfe25166969d565..96f7ae6cba458abfc2f122e3f0ec2610bc903198 100644 GIT binary patch delta 281 zcmeyS)S;zDECN*=>~IY_in$#1|n^D7q2*VUk3ydE-InjLL?0dzd`i^ZvH~SF2^1|N89l-}k>y zPwM&WFiX5dt!0MvB{!2@enOX2jI)nSnYVI<^rb^tQ~E2v3TSVUs6V}H(~R)D{<_nA z9o?RslIV&L2oBTdeajHIXli84q@!}8(Vwdd`;#sBE|CM5nPyXfA*)^Z#i1{IF@peOLIP*2$f`l8c@i(^_UE^ivTP_~{r^@K@ c^m`{CJ-bmFcmLa6o~o|D%v_Cvf#?AM09>1h-T(jq delta 262 zcmeBB{-(5{QlOq=YgC1bzvdKiCl#JwyRwckZDCGMirVxt=J%bo>gR7785qbUc%44u zx%JZZ>-@TYx?0!tHBR|^dFgujp3~60R_>|mefFfjhwdePO)sBw=gxTd`+Mqm_V}MW zukCr}409Lr)aEb8r>l!CO?xt3Z26(kSI^a^siYryzHFLG)0Y>`b)3us7fdhsni}~V z`+AvNHQnf8eBIR3&*+k`kE!WpQxk6!UvE=??~MkgSF}BJv~+xRA95G)aPgG#CRVAQ zJ95yZapKaZwN1%P%FGiEA31b{ZHa>qo5AEp#uJH&NheL8B&=NLnr39sm?FtAXSF-J FZvpY*ZSnvB diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/ed38ec79-f365-46e1-aa0a-6ff5e030781a-m0.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/cb26b00d-1cfa-47c8-b859-bc2936a263b0-m0.avro similarity index 92% rename from examples/idsse_warehouse/tracking/match_metadata/metadata/ed38ec79-f365-46e1-aa0a-6ff5e030781a-m0.avro rename to examples/idsse_warehouse/tracking/match_metadata/metadata/cb26b00d-1cfa-47c8-b859-bc2936a263b0-m0.avro index 36a0256094c75cc95b9a0dcebc682297bc08c5a6..05cbc2fc2bf699e450d6a93267d084faec4ba0c0 100644 GIT binary patch delta 282 zcmeBF?o{4TDNxUItu4=AZ{~s&+r!(N9{X)zy2V`Dz1#1wfk@lk#Vbx7Idec;Vm6D@ z>9Cas3R;X=tg}ns%$d}2XG3g9O9g}3kt1>Q+GnySq^GF9JaEkZkNHdHiu%8i_VxSM ziYC8do>uAT?$pt${Mscsd+o_dC-(>k?^?bn?*v!=#XHZg_Ih1hTyIqR)NuW-`B6Hv zo93Jde(qtrVCkAT%U{eco%d2!sr1`wnWYZ7}`>-e5xr?|y7qlk3bezj=Y|wuh%1N|~7?$}Wa)TiH{p+%Wg$Ny+)X d&;EoQ3*Y?OprYp2`?L$eZ^RjeP6wig004?OiyHs{ delta 282 zcmeBF?o{4TDNwIc(w~{uwR`*D&p9sgFW*`+-C{27w)Q*hAke0+tX<^l+qmQ4rtFy( zje0=~92yu@7j02{ZMf}2_g&ryr#p(aiyn!@G|g($YhQ50&+&NtAN`lkG4(b1_wL8l ze`9f3{}hYW;nV&zL{7RK8MA4z+|=mcy)8m>T7<7XR-V{)(L=^fhC|6!$?LM< z>VRH#Mp=^-xv)27>z0{c@1AU#S9bCE`o`;g(e<_sc?;SP>;2-o&2@p7|9jf<=Z})C c#M8C*?B%I>|MTg7=?%*th|36N1)_%l0GZv0CjbBd diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/faae9c91-7f9c-4bec-b401-856370f90b74-m0.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/faae9c91-7f9c-4bec-b401-856370f90b74-m0.avro deleted file mode 100644 index 3148d2b3e9ae266a43f7f8c48dc63bf9234b279f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4492 zcmb`KZEO@p7{{Tl#3(5rcrh4a#?{2c-r?RO=%pJk_vUVuKEmauQ{(r!grZ6%AtQRLb@4mJD6W$qC8A zCH_VUGp+9-SW86q7}hrU-z7Ue!zJ)KsF0zAzf}phYIeyX%wUFX-Atf}O&ThsLj_7O zo#*D}aC3<;tYe1N?VxdU_wxzK$CKm$QVHNdEM@>0H!0}AItP>=TH|2X|Ewf+h}O`X zm*Ok%W?gDc(RKk|vY)sV2-GPnaKCG>)oh(;jXEisn2|=!wisBso%^{;JokOr_ec;T z1ei731^y@u5`rlC;Xx2V%sd)C70#m+yzEC9NruIwp#+NTu&_h>Y^L&{PK)~6Z_JoX7>yBhz6Ae{hcMcf1p2eZCP zL#b%Ep`t}~hPFCw4f7I2&vbX70fY6S_RUCS8HwkG(D$9HvJlA(-ZSDB7W6e?#Z=jv za|C!&24&#NDTRMdK=|pPL=g`(VxD7?g#XzX6)+__0wk=DaMpe?BVNpLB!20sn(aXm zfq2jdAck@(lGc8al&yD(GC+0_`3e*nzA?@?ZbDF}#*0KXEVVGmNM$X{sc0UFYAhS- zu^BcI*Fao@oAEVb<*C`GSF%+8Mzwkd)TVnySoG!0$F7S9`KKFCR8qA@DQYJHiK&^M4t+x$9tc}H>cU-dw)|L8X9Fxp8r`h}R%8xX zSvX)tUI1XFc7QoABzQp%^9~hr$HLaEFw?EI%+_=UV5YMRhnddAyq{Cyr?a($$tM@y z4X%7%rEdY0z3V~#_At?{n60Oq0^RzWIGVey9w9Jln##2XWtsPsz z3JF$mh@Ruz7-CHiCWUv?^euE#r7=#N?wTE*o)d~+==I3EMzg@$GL6OHN zPna`w!iZZkEP`Bn00kcM`7Qig2b&D82{7EzwH4597nr*`6i@Xfu6d?YQd~f0i+IOI zm{lE@P=4u!W14~qxQc)Yeo=u**`UG|f20Z~J{N{=5u0G4ge{{;C{sbk9h8O^;YEud z-2dUoPrr6=_-Ewc=z+TP_1^0zpFH*Gis1)FJ~;f*Cx5J*7$wIizDtfvhj!t+&N#&@ zry3|NK3iXyEL@~pp4)PD(T11bd135}RsH{-erfNr^S@1Aynf;GU&aM_V(YzM-ueCV z$Im8LH{HFtsco43JbvKo}_RQXExnu9|zb)6v^4YWR{eE=SnIov_&sRQv>xZ$i z@vYBatKaxq*S3z1#@#*Ro38x0|LChLCU<@QcJsIEdJdns+h-lua=yKfPzM(bipw;D2R@tV#d? diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-1456969541151746380-0-b3defa57-62bd-4a60-87b5-0d6fcafd53ad.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-1456969541151746380-0-b3defa57-62bd-4a60-87b5-0d6fcafd53ad.avro deleted file mode 100644 index b1ed9f2bad58ae0a490c1dd4f9706287219ff4be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1824 zcmb7ET}u>E7`83cq$E+4!eSU~NpP1L-Cft9EUD-Vk~FZyrn56=-O-tI#+kF~q6<>q zNtQumP*6}91z}Ku5d|`*AGNv1UW5 zfq-VjF!9Nfmk$I(b)mXofaimJZ6H(=<|;IxLqvSGuzVC(lx+s=ibEtqABw9HsC)SG zDArX#eBDqtWQ;u3C7@f!ei2JhER>fH($j+^?czih9?6tDy#?y%Tg$23}5+;UvsG9d7vy5T-BonBb8$2KFzTW2a0~r}~ z3{xuU8(=Zk*@hs3u>t{dE>9Xxe`s*(f@ILVnNQ|8WUF8;nl}1L4-<$hgr;k%FIZ{? z_ds2U(9v|PGjBUOHr@n*2D<`lT+^0iwH=vNu$n^FiPVE!m`x^E(C&l@L4^cJfB=V8 zHlzPjJbRzbO=6QdTg&}Zd4G!zf1Nmm^gnmTJuf}Gh8}Eu&TZO1GIo;%lC10J;?dP{QTKgnrOn{sQb zps)4c66f2dA_t}@PS~?(`K?%{vy2fvyQWQ3a{U)Hqdm-XS&PO`ukaJsqvsY+&F5NI o60}58Epiph_N30e<%bhDCcLq%IrjUvp_kds`T8199MK&O0QTB$_5c6? delta 242 zcmbQsH=9q@KPiimN31w6v7k63zeG1PMa{y*)ZEP6+{D7r!rZ{j*u==taHFaTTfJ!F zwpPcs)0@un9=@_$P$HRWACqnOM!qHk5!a3XgS(xWEncfzn6H1UEzme=+k^JZm^m?j zIK<1-!-X$4KH>X*XHK=+vb=)B61S^M=6yZq)yWqsYIFNhNAKH<8y`5xzFH?D#l(@i zo1xVFt<=+iE@g`~ZypqWi{N$)U38RTKl^NF@2pQZPQ+Y}NuRZhbNZGCI~L8Ey;VoR lXsz(AW8M;TYu_*GnDF~$+20ts^Vgevew-J7o#24(WB@4rY&ie` diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-1114700784421547592-0-ed38ec79-f365-46e1-aa0a-6ff5e030781a.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-3611677702575916172-0-a858ff54-e99b-4dae-b66a-ac94a48da697.avro similarity index 77% rename from examples/idsse_warehouse/tracking/match_metadata/metadata/snap-1114700784421547592-0-ed38ec79-f365-46e1-aa0a-6ff5e030781a.avro rename to examples/idsse_warehouse/tracking/match_metadata/metadata/snap-3611677702575916172-0-a858ff54-e99b-4dae-b66a-ac94a48da697.avro index a83c360826eebcefcb86866855515ba1a21b7558..48312e09fb499da495a63ac761e7fe18cb4ecc90 100644 GIT binary patch delta 340 zcmaFE_mt1aKPiimN31w6v7k63zeG1PMa|gE(9q1>+}yy()ZEn4(9F=>NTndLC^fG{ z7e$$gv5}dH5m=R}nWcrXkDC%NFLN=M4Q08%VG| z(0Enaa^BLqB?p2TcIUOLKUQemSm$wp@A=%gh4R*2drA!_ z)m1h+^w#&8$XD>6G+VZur9RwFJYedo*Iovfq-GX4@GNCv3*#0z)p$kwnz;D}&VO$e zPm6W`{qlF8ZEf8So74Y__lVs~(%h&avwN$l!NyHp3s;#qN55Ly5~R0r$Ajg@cIWpm Un}7J-7W)~J|GDFKx1jqL08gHiNdN!< delta 348 zcmaFL_lD2MKPiimN31w6v7k63zeG1PMa|IA(8S!pz}&*b#K_Rp#N5=WWZj!bw>)PGnkV$t-QMr%BILeJbB*jSlzl*g=7ODhzgC}0I&|uXUE<~U z<+~2&)n{mKI(E$CsxI5AzrIU89@Fe;vnOTJhCIxXzq=sDjb)16whRzrv0=ORY_n zjKd`=y8HR_>xFJDbac;tD>F?zVdJawvU%)VSl6%G+03RgsaRow z3s>gC#_pQM`+0wzOfjsx6T5c4+}C#>z0_iimYr-ga=2s^BkCd+`D4G=ksjwm>63Cl bBp*2b@$-+Zd*bw`Jr(=U{e|N=dI$jkaz?D1 delta 396 zcmbQiKbPOzKPiimN31w6v7k63zeG1PMa{&*3<%7P%*_le%}fo9jg2i-3KEM_^GbA) zl^GfunwT3Hm|K{b7#W(Hn44N|^p<6-KY3!#i6^d~9bdT4TlVSFJ3qE1EJx4TdmV9* zVErI5&9-gp+2=k!Mw27D%{TDfKU1@rGw+{)aCkJgsI_3X=tiOU3&d|6a*~d==11t*qxHA*L%Mom7J eG401}Ry&^M{rT&cr*HR<*8Zb!a^g68FaZF|EU$|I diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-c6360589-6439-4604-88e4-272cf35f75ce.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-7a3c0cd8-3b69-41fb-a134-02dcfe960e12.metadata.json similarity index 90% rename from examples/idsse_warehouse/tracking/tracking_data/metadata/00000-c6360589-6439-4604-88e4-272cf35f75ce.metadata.json rename to examples/idsse_warehouse/tracking/tracking_data/metadata/00000-7a3c0cd8-3b69-41fb-a134-02dcfe960e12.metadata.json index 496040c..f6e892f 100644 --- a/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-c6360589-6439-4604-88e4-272cf35f75ce.metadata.json +++ b/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-7a3c0cd8-3b69-41fb-a134-02dcfe960e12.metadata.json @@ -1 +1 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"43ab1002-3f72-45d8-b6b0-af518f137ac6","last-updated-ms":1775647655854,"last-column-id":11,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[{"source-id":1,"field-id":1000,"transform":"identity","name":"match/id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"snapshots":[],"snapshot-log":[],"metadata-log":[],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":0} +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"dc3f4077-a93e-4532-86c5-245085510607","last-updated-ms":1775648079892,"last-column-id":11,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[{"source-id":1,"field-id":1000,"transform":"identity","name":"match/id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"snapshots":[],"snapshot-log":[],"metadata-log":[],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":0} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-4bae733e-a73e-4da6-8812-af9a7d6b04e9.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-a4870a2b-6cd4-4964-a16c-803a68332e07.metadata.json similarity index 57% rename from examples/idsse_warehouse/tracking/tracking_data/metadata/00001-4bae733e-a73e-4da6-8812-af9a7d6b04e9.metadata.json rename to examples/idsse_warehouse/tracking/tracking_data/metadata/00001-a4870a2b-6cd4-4964-a16c-803a68332e07.metadata.json index e70c421..ca9940e 100644 --- a/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-4bae733e-a73e-4da6-8812-af9a7d6b04e9.metadata.json +++ b/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-a4870a2b-6cd4-4964-a16c-803a68332e07.metadata.json @@ -1 +1 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"43ab1002-3f72-45d8-b6b0-af518f137ac6","last-updated-ms":1775647659807,"last-column-id":17,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false},{"id":12,"name":"vel","type":"double","required":false},{"id":13,"name":"acc","type":"double","required":false},{"id":14,"name":"entity_key","type":"string","required":false},{"id":15,"name":"id","type":"string","required":false},{"id":16,"name":"jersey_colour","type":"string","required":false},{"id":17,"name":"name","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":1,"field-id":1000,"transform":"identity","name":"match/id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":377391038018914117,"snapshots":[{"snapshot-id":377391038018914117,"sequence-number":1,"timestamp-ms":1775647659807,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-377391038018914117-0-d962db46-093c-45e8-be4a-0c6d572dad92.avro","summary":{"operation":"append","added-files-size":"40895970","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"40895970","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":377391038018914117,"timestamp-ms":1775647659807}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-c6360589-6439-4604-88e4-272cf35f75ce.metadata.json","timestamp-ms":1775647655854}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":377391038018914117,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":1} +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"dc3f4077-a93e-4532-86c5-245085510607","last-updated-ms":1775648083318,"last-column-id":17,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false},{"id":12,"name":"acc","type":"double","required":false},{"id":13,"name":"vel","type":"double","required":false},{"id":14,"name":"entity_key","type":"string","required":false},{"id":15,"name":"id","type":"string","required":false},{"id":16,"name":"jersey_colour","type":"string","required":false},{"id":17,"name":"name","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":1,"field-id":1000,"transform":"identity","name":"match/id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":4345927569820710336,"snapshots":[{"snapshot-id":4345927569820710336,"sequence-number":1,"timestamp-ms":1775648083318,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4345927569820710336-0-3faa5e91-e6e1-4926-9601-15797fb2ce84.avro","summary":{"operation":"append","added-files-size":"41845491","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"41845491","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":4345927569820710336,"timestamp-ms":1775648083318}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-7a3c0cd8-3b69-41fb-a134-02dcfe960e12.metadata.json","timestamp-ms":1775648079892}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":4345927569820710336,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":1} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-8e9395a3-7b87-44a6-acc5-96b3d3d6fd5d.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-8e9395a3-7b87-44a6-acc5-96b3d3d6fd5d.metadata.json new file mode 100644 index 0000000..e1de786 --- /dev/null +++ b/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-8e9395a3-7b87-44a6-acc5-96b3d3d6fd5d.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"dc3f4077-a93e-4532-86c5-245085510607","last-updated-ms":1775648086130,"last-column-id":17,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false},{"id":12,"name":"acc","type":"double","required":false},{"id":13,"name":"vel","type":"double","required":false},{"id":14,"name":"entity_key","type":"string","required":false},{"id":15,"name":"id","type":"string","required":false},{"id":16,"name":"jersey_colour","type":"string","required":false},{"id":17,"name":"name","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":1,"field-id":1000,"transform":"identity","name":"match/id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":4114664439361220711,"snapshots":[{"snapshot-id":4345927569820710336,"sequence-number":1,"timestamp-ms":1775648083318,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4345927569820710336-0-3faa5e91-e6e1-4926-9601-15797fb2ce84.avro","summary":{"operation":"append","added-files-size":"41845491","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"41845491","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":4114664439361220711,"parent-snapshot-id":4345927569820710336,"sequence-number":2,"timestamp-ms":1775648086130,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4114664439361220711-0-0e34766b-2d66-44ac-b491-723a4772a1a3.avro","summary":{"operation":"append","added-files-size":"38882489","added-data-files":"2","added-records":"3832763","changed-partition-count":"1","total-data-files":"4","total-delete-files":"0","total-records":"7919839","total-files-size":"80727980","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":4345927569820710336,"timestamp-ms":1775648083318},{"snapshot-id":4114664439361220711,"timestamp-ms":1775648086130}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-7a3c0cd8-3b69-41fb-a134-02dcfe960e12.metadata.json","timestamp-ms":1775648079892},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-a4870a2b-6cd4-4964-a16c-803a68332e07.metadata.json","timestamp-ms":1775648083318}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":4114664439361220711,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":2} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-c4c023b8-fa71-41cb-a109-30c7bf7cadf4.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-c4c023b8-fa71-41cb-a109-30c7bf7cadf4.metadata.json deleted file mode 100644 index 83aa632..0000000 --- a/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-c4c023b8-fa71-41cb-a109-30c7bf7cadf4.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"43ab1002-3f72-45d8-b6b0-af518f137ac6","last-updated-ms":1775647663662,"last-column-id":17,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false},{"id":12,"name":"vel","type":"double","required":false},{"id":13,"name":"acc","type":"double","required":false},{"id":14,"name":"entity_key","type":"string","required":false},{"id":15,"name":"id","type":"string","required":false},{"id":16,"name":"jersey_colour","type":"string","required":false},{"id":17,"name":"name","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":1,"field-id":1000,"transform":"identity","name":"match/id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":478866493446175361,"snapshots":[{"snapshot-id":377391038018914117,"sequence-number":1,"timestamp-ms":1775647659807,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-377391038018914117-0-d962db46-093c-45e8-be4a-0c6d572dad92.avro","summary":{"operation":"append","added-files-size":"40895970","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"40895970","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":478866493446175361,"parent-snapshot-id":377391038018914117,"sequence-number":2,"timestamp-ms":1775647663662,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-478866493446175361-0-7ad1e2c6-167d-47a8-a02b-391e6ef92b65.avro","summary":{"operation":"append","added-files-size":"39112103","added-data-files":"2","added-records":"3832763","changed-partition-count":"1","total-data-files":"4","total-delete-files":"0","total-records":"7919839","total-files-size":"80008073","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":377391038018914117,"timestamp-ms":1775647659807},{"snapshot-id":478866493446175361,"timestamp-ms":1775647663662}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-c6360589-6439-4604-88e4-272cf35f75ce.metadata.json","timestamp-ms":1775647655854},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-4bae733e-a73e-4da6-8812-af9a7d6b04e9.metadata.json","timestamp-ms":1775647659807}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":478866493446175361,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":2} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-34c7dd19-5940-44b4-8fcd-39d9dc910155.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-34c7dd19-5940-44b4-8fcd-39d9dc910155.metadata.json new file mode 100644 index 0000000..8fd9e66 --- /dev/null +++ b/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-34c7dd19-5940-44b4-8fcd-39d9dc910155.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"dc3f4077-a93e-4532-86c5-245085510607","last-updated-ms":1775648088751,"last-column-id":17,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false},{"id":12,"name":"acc","type":"double","required":false},{"id":13,"name":"vel","type":"double","required":false},{"id":14,"name":"entity_key","type":"string","required":false},{"id":15,"name":"id","type":"string","required":false},{"id":16,"name":"jersey_colour","type":"string","required":false},{"id":17,"name":"name","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":1,"field-id":1000,"transform":"identity","name":"match/id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":5344841726861284858,"snapshots":[{"snapshot-id":4345927569820710336,"sequence-number":1,"timestamp-ms":1775648083318,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4345927569820710336-0-3faa5e91-e6e1-4926-9601-15797fb2ce84.avro","summary":{"operation":"append","added-files-size":"41845491","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"41845491","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":4114664439361220711,"parent-snapshot-id":4345927569820710336,"sequence-number":2,"timestamp-ms":1775648086130,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4114664439361220711-0-0e34766b-2d66-44ac-b491-723a4772a1a3.avro","summary":{"operation":"append","added-files-size":"38882489","added-data-files":"2","added-records":"3832763","changed-partition-count":"1","total-data-files":"4","total-delete-files":"0","total-records":"7919839","total-files-size":"80727980","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":5344841726861284858,"parent-snapshot-id":4114664439361220711,"sequence-number":3,"timestamp-ms":1775648088751,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-5344841726861284858-0-78582c4c-10e1-429d-8391-918bcb86bb3f.avro","summary":{"operation":"append","added-files-size":"36045277","added-data-files":"2","added-records":"3430350","changed-partition-count":"1","total-data-files":"6","total-delete-files":"0","total-records":"11350189","total-files-size":"116773257","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":4345927569820710336,"timestamp-ms":1775648083318},{"snapshot-id":4114664439361220711,"timestamp-ms":1775648086130},{"snapshot-id":5344841726861284858,"timestamp-ms":1775648088751}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-7a3c0cd8-3b69-41fb-a134-02dcfe960e12.metadata.json","timestamp-ms":1775648079892},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-a4870a2b-6cd4-4964-a16c-803a68332e07.metadata.json","timestamp-ms":1775648083318},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-8e9395a3-7b87-44a6-acc5-96b3d3d6fd5d.metadata.json","timestamp-ms":1775648086130}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":5344841726861284858,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":3} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-53fc18be-4237-409f-9d8b-49218b2e1ef4.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-53fc18be-4237-409f-9d8b-49218b2e1ef4.metadata.json deleted file mode 100644 index 5926bf0..0000000 --- a/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-53fc18be-4237-409f-9d8b-49218b2e1ef4.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"43ab1002-3f72-45d8-b6b0-af518f137ac6","last-updated-ms":1775647666709,"last-column-id":17,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false},{"id":12,"name":"vel","type":"double","required":false},{"id":13,"name":"acc","type":"double","required":false},{"id":14,"name":"entity_key","type":"string","required":false},{"id":15,"name":"id","type":"string","required":false},{"id":16,"name":"jersey_colour","type":"string","required":false},{"id":17,"name":"name","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":1,"field-id":1000,"transform":"identity","name":"match/id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":4761525111313210917,"snapshots":[{"snapshot-id":377391038018914117,"sequence-number":1,"timestamp-ms":1775647659807,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-377391038018914117-0-d962db46-093c-45e8-be4a-0c6d572dad92.avro","summary":{"operation":"append","added-files-size":"40895970","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"40895970","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":478866493446175361,"parent-snapshot-id":377391038018914117,"sequence-number":2,"timestamp-ms":1775647663662,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-478866493446175361-0-7ad1e2c6-167d-47a8-a02b-391e6ef92b65.avro","summary":{"operation":"append","added-files-size":"39112103","added-data-files":"2","added-records":"3832763","changed-partition-count":"1","total-data-files":"4","total-delete-files":"0","total-records":"7919839","total-files-size":"80008073","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":4761525111313210917,"parent-snapshot-id":478866493446175361,"sequence-number":3,"timestamp-ms":1775647666709,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4761525111313210917-0-4665052f-9f33-4c4a-b55d-e1fd2bcb223d.avro","summary":{"operation":"append","added-files-size":"35830535","added-data-files":"2","added-records":"3430350","changed-partition-count":"1","total-data-files":"6","total-delete-files":"0","total-records":"11350189","total-files-size":"115838608","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":377391038018914117,"timestamp-ms":1775647659807},{"snapshot-id":478866493446175361,"timestamp-ms":1775647663662},{"snapshot-id":4761525111313210917,"timestamp-ms":1775647666709}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-c6360589-6439-4604-88e4-272cf35f75ce.metadata.json","timestamp-ms":1775647655854},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-4bae733e-a73e-4da6-8812-af9a7d6b04e9.metadata.json","timestamp-ms":1775647659807},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-c4c023b8-fa71-41cb-a109-30c7bf7cadf4.metadata.json","timestamp-ms":1775647663662}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":4761525111313210917,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":3} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/d962db46-093c-45e8-be4a-0c6d572dad92-m0.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/0e34766b-2d66-44ac-b491-723a4772a1a3-m0.avro similarity index 78% rename from examples/idsse_warehouse/tracking/tracking_data/metadata/d962db46-093c-45e8-be4a-0c6d572dad92-m0.avro rename to examples/idsse_warehouse/tracking/tracking_data/metadata/0e34766b-2d66-44ac-b491-723a4772a1a3-m0.avro index 31c7dc227f208ba25227159d2e644dd798424fd0..a35316ac6b925ae4de6a22eff4e5377d6b635e25 100644 GIT binary patch delta 1233 zcmV;?1TOpCG1W1!zXFr@0xpy70t%DA0xq*90|60#LmZzDbJq;dxTaQz&)kZ>0-y5xuONpC!S9(4;>?iN7=mB8EvaabuHg+}*TRddQ(( zyc9%4rE07Qf}uwbqNUA2if9oJ9t5F^Qja2Sy$HJB7J?BE&M*%%!+bB#3`0??%bROY z*XC}28wjCBPq9=c{feSwOk$Wy&Cqi>VvxLMnaU8EAZ5Kq48<~Ve!NsYpDkbuXY+d1 z!lkOcPcGp~txQa%R4`4Fox}#w^twqp1>Kd3g9^6t+P>&Wsc?8`BrP9Jj>)6p*y&>t zB@9irJBg`@cswUZ3-P$DsyHv_)I$+D5shJgHIazo2#y`BVdG+*SnP@9sgs%EF-qtT zd-vr^&(-I@L@GqR?WN~w_dLy9fAXf+(|G*S?Ooi-&g|m0+`dOQzS91cI}d5Td2L7F z#d@nJxY)i&3#+Vn*ClPxp=T~+M?e}4a03{C2jB&;01n^-_yIg100;sEfC!KPApqom zqCY~B2YC^TxRu#q-}eW7etYnrmp7<@Jqg-FKq4$9KnQ^BF8+OQhC`X=ht^LQ@^IXK zj${fX`%*mf`qj;3M{*#@EgboH+0WwvLFklC1O8ue z0wIcF-haB~nQ8vm-CDmd+CIMl$@2+A9G?zz*9_0NrdEf~+={*e(g>B&PiWJBP#g#F zmvoL$N_Y!lloGm75%mz$w4td(Wvg?@sPo@CFU9hjzAn|asY&V-b{L}v-Nm!p4n>bj zFJ8pc>^!Px5tUv94<5>(o;>(#28D_jfAD#MknfLg5|;h=cF=p%+uLyvLYskdrAlX2 zRn587an*)n7mL)PrscWn0=-Xvt9FArs^<{%Zl%6fC=rhoOuO!pN*(Ug2c*`hQdg~% zT$dKsi9;>B>C#bPJgNFejd-SYBC%L0onKhYDp%%Ll%;s`*0QF?hofjpoF;W6l`1NU zQYxkBIx&@^epXYAM3U%+ksumLo@o&0UXyzKwfP&@bC*_FX?$7x@$!#j*Tiqn3eznn+~2(%>AhN&I)UgGD}Q2Qk5~k0 z*fRkp0SAI01bBdf07Nhi!XN@95Ct-bf#Di7T7rTogm{FJaQJ*m1b!5*5g?&x=leMs z!~lW@(%&z}VU%lsefj%;7z$#168RGn%N57<3KprBdH?yVnI#22re-8CN7l(hfjb!F zyCQcwKh+K6U#Id{Wla>h2T zW=+=8divib_xJ8}{F41PdsGs;mTh}>)8hq?lG?0g*C^(Or@$dNbUPz*J5Tvc7#|FV v^=9TG_``dc(1kfk8U;3n$A5G~EDOugA98HyLmZzDbJq;dxTaQz&)kZ>nvGOL delta 1241 zcmV;~1Sb2{G2Jn+zXFr@0xpy70t%DA0xq*90|60#)D7&Sxf)S05&qiW7OO$P0-_0H zV(OZEWW~vo5BG2}FfcSQr)B1(TIuWShZd(673&ui6a8GCTC~nrN^fvmL$d}=jW9qX66CKQ!5g43vyD6^)pk7i&Nvv6N^$a@=J?T;TlkN z>Vt`Y+{BXP3{@ky%oJM}Hy>SJ#}HjF1LJVt2z>((&^6Fau{1MENis3hHLx^J)-^Fr zwa`sUHA&PpNH$9`H8)C0OtCc5D@ZIVEKM!p3vdhy40R1*l0h}>*q%1#GoOCRF)1-! zVrN;z!nTNo|F15t!Lx8#m)DNM_>VeyU>SwpIJn{G_Pb4>-)(7zZd}NC-gmGBijDeA9^OKlD}pfQ$mF-kL-?uxZXdp zH8IyJBR@A)fCEGdafqyobrRuNQXu9e#__P~nxlk3>+8TBa;Yf}aQ8XL$iTJLfCwU8 z$H2tF@veU>=d*Q3gfDbVmIFEk0Bz+8)D7&Sxf)S05&qiW7OO$P0^kUh&_QT_(@+=( z@RxLsP)dj)&Qd}bIz%rqHEZiS5ZZ2bI97DBqKKosCa)%}ZMr0N8|+ZDmEy&NH&MLw zAToO1VTaLq*+D#b8&Ytmffp~z5D$LMpiuGRgWn50^8MdGq3P#uKD;|T{JQ5y2sL~q zvrJ|cMaetFc9fcJ6^q0sn&CQs$~<{Y%2tioifdzS&8$8u=-9;t&8oWCtin0j#Fbi^ zIEtw|4k>J4n;2HzA;W>uNyXbL*wu_nYSz?m%x9P6TzW-bj85On-B+T6C0~>EWJ1-8 zv4k8=PHS>3PG;mHiD5aaCG_}pRmXZ#y;{TedY!oJo%G%1{LK|w91VMavRgjwv=}-< zf2YQp)OeF(ItOjq-#-3K1-d@A=i|Om{Iip{l<=$nlA35z!S^4ZQlWl(T|D)Lo3wO9 zMPAbg0w8J(i~|PvK>)CT13VDG1PFo<5J4D7AOZ$4Xc&V0D1cbRA-;cdUI5-GJVQW4 z;m)TA5{Li<4aC2{jp6oxjkTkl-{+8@<1Qm_1%#4mJ8r?i<}%4}R#aG{@i# zyu~wAl^XU-iQ;fzd$9dSyAnb)&A|140IgE;)D7&Sxf)S05&qiW7OO$P Dafd*X diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/7ad1e2c6-167d-47a8-a02b-391e6ef92b65-m0.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/3faa5e91-e6e1-4926-9601-15797fb2ce84-m0.avro similarity index 78% rename from examples/idsse_warehouse/tracking/tracking_data/metadata/7ad1e2c6-167d-47a8-a02b-391e6ef92b65-m0.avro rename to examples/idsse_warehouse/tracking/tracking_data/metadata/3faa5e91-e6e1-4926-9601-15797fb2ce84-m0.avro index db140740feb4da36dc6efb0f2dd545ae5772d1cc..2d0872779959c9e8fb0cbb87bcdb560a6707f285 100644 GIT binary patch delta 1227 zcmV;+1T_20G0!ouzXFr@0xpy70t%DA0xq*90|60#uubUj8(L?k0R?X!M8czX0^SL2 z(r-u;VH^kWhufS_b6wXujxnz5IEi|5-ZtHILg1z?GNlZqUL@mjd)5uN-MTwd!xtmc z7ZLR5Wj*iWT(2d=VMlGeJ^%j_1SiaKA6#JC33X8?}Xn zncC`q1OT95OQkcoQ&E&51M7y8*R_!mtmA}g8cGkojWb#v>x!wP#ArHqGoC~yiYK(3 ziPAai98aKZK7$PSU3bP)`S9>EyYdSfAV<8I*f{t=Iam zD?YMhpWK>4D2T&;8Hcc6ZVLxOayaDk%l=@0d$>I{5=h{Vww657Z;fG-z7n}QFm!&9 z;tz&Z$}_g=uT6oHsA{8q*6x_K!^)eljn199io>n-sYx6+3U)!QYfNSgt)Lu=-P&Lie#!T96VwCMvwJc^J(GA(saFm7L2Dm5(Ki^r8?Dy;PGSjtz*8)y< z@%;YS6yZ?23s$>WTPzs!Ns?79iYBq16Z==nxrCYmfP(PBkF_Iz=Bp>xA8((3vai&Kk= z^$Uvflaf-4Qj;@EiuGMm%Tjal3sQ^pONtVcvorJ3<5Ln#662He^GXsk^MK;16^Xe8 zIjP0^nJLA^sqy8BMX4G2rNya#a1E$B^}$4LVo7p_s*zh}imi*AkFKv{h_07`aky`U zz5xj68tNLSB_^7tS{mx6nxz`*nphf{=~|i@80s3Dnp>KuB^f2BTA1h+Bo-Bxrk3yp zI0glVx`r^xpc>Y+BWKFQ8JtW?Odl9oS{T_{7&-dRtYqR^cJ&7%&kAOLz8%a0FBpZk zJ=o4DvZQ-HqgV^0#F_`qj8d15&SI4Lz%19osL;Zw)WXOBGy!N93lOsbF$WNH0Wl8{ z^8v8{5DNjZ2oQ?_u>=rH0kI4a%K@|1O8urhE72&glF0gp&pW>IlTd`4nU8Xs7_zmu0PFenX; zA_O=R%M&YwIE;;B4~cNBa~C}%A@EHjrvK1a`2$h{5d91dG76}=YBI1|-S9}cVjbA( zS!G#da_@zt0@w*{ z(obm9P#g#FN0xP2ts&M>N{KN&`F;5&gdom;x%&KJ z{m$Kg^#Fj;7Ohx9ow6)vEM!`8)iiQBWTL!oTXGLNgGxpfnX+xd{9v(iGFyN)%;t@X z4T}}$9G!*bY6)3#v0zy!I|NOn8#N294m3~7E@f!v^{wIFVqterZ(7=y?3enK$g%yZ ztl%XnQXCdk6wb#aH5M;O(Kt*OB z4x9UQsb%5vJDw1UH*I8{^wdde>iu$?_rdtQ$Mb_%#~BI^!C^lIz=;`_(zM`AfWkjUkXe}pqakK_ z`@of%pPQ#Sp?P*h=iRjKbjCeVQUbkyMU6z_v4kk3;4m@;B`Tzy-^d)S{lDw|3x4Z}8SHtn|&EO+Wg88H;rEtz7e9RnLu0qVu0)_}}l1*q6K^TU=+0?LvbeV=Fgs{XA5WQ?SKbqJ=Aq};M zpw&u4MU*f}X5G~!8+SKW!9yu4;z9MiAD%JlWcK(S0?44gjk@-K_IzP18yaw;iozTa^m8dDU!X zEoR?tb62`FdwH>RZb?u^!#a-^_TFv#g@mwA#ysNpi1>PUhXfAR*GcfDPkQR3pGj!v z*&2~Q{Ah$d64`yzjBY%dJ={cly%c~+_J&y zx->k0u~1Y84|%0TMYh83g~WdMYEKq3nekqP9$K&(rN2&Hd))hZd{c>!TKA3rS8qPQ z`F>cv-w_V0sbg4{Yqeb|ZgU;K4WIrKHAzLoYVZ)277Mbd>l1}82<>-UGl70SAEnRr1O|9)KW-nEe}#o6flid5YFDlJ!kKj`1aUQ?Gk&NwWX| diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/4665052f-9f33-4c4a-b55d-e1fd2bcb223d-m0.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/78582c4c-10e1-429d-8391-918bcb86bb3f-m0.avro similarity index 78% rename from examples/idsse_warehouse/tracking/tracking_data/metadata/4665052f-9f33-4c4a-b55d-e1fd2bcb223d-m0.avro rename to examples/idsse_warehouse/tracking/tracking_data/metadata/78582c4c-10e1-429d-8391-918bcb86bb3f-m0.avro index 5bd149bca07025dafcc0050a3c712c23d8787bc4..4f1ca86aa13b05ba0259c90c9bf8619e53f96110 100644 GIT binary patch delta 1211 zcmV;s1VsC}F~2dezXFr@0xpy70t%DA0xq*90|60#l)<k%bMjY~%fb`bpKJfE?X85>4hZC%coq%Gu6gr6&?Q z-*-lhkflVF6Y*p5sNSY4S_ErKTQreT;<1E(rX;j@T2IH1r_-_Q(Gs#Qm$Ae4cMcAW zoF1lx`mpKw>4t^1SIv}6tvAtgv~P}PR-R8W{Z z@~IK{A#4HO3UJw+WjiSY<+H5!eqUFQLVkAa0%XuQx)x*rxLD>@pdAA{StdPk<=2=s4OQ<%PAL;)cs<~JsbIq`QNhO z$<}VGSGG{0_R2!rFipoSJ8aOwsL*bh1q>OY*RqRa-e2dtcxJRR)4AWWBMfhT8CVqG z=^cWwKQ=+A@2F{R$E3GpX=v!2)PszoG>FG=(bfT=7-sfm&A)VKhkLN~T6Ft=`~hJ` z1eC$I`$HXQTr!m)s0I$bqyp>*ZI3}{(@+@4zdYv%rIbg>N(p7wg0u%?l6IS|ploGU zEt9Eicu~qr`nm+$rY5OV*kOm@ak~t>cv6odLj>_QdX%~AJcuVn5J3^`!GriMg^tQT z_`Uadyx;%NBjne9@ag;B`{!GK0syQZYuHVGPF2-qkGr1QaUH|pE;lXTQ|I|T-gG+L zRehJ4ckK2`t^9!>`>fSza!S8ZQD~g$ zF!yek`>|_tH?A*VTq3E_u&p=p#_P|7WXKO;ED**6LHzc&8~M6@CHj-ZUJ<%0#NR&A zq(9G2Cjue4v3F8__$ZtDNG1az9SE5~0CX{B3>im6B!Wbd7($UaA|VMRiO5I_nMBe^ z204gXv;El5emM>iO0y77RBG6+=hrOOY{bI% zi{+}KY5AFHor-LgJ&23_T%{+8?{+78N!rid+U)&4u_M!g59_J)sI|t9tcI5#`nj;W zwMhom)UzDNce?&zaaGe7^a>R%r^Vyqi%Yl5lDPkLqC6~~l__o_c)l=uSUj-SYyd(; ZO#TOBUCNZfxBEjKXk0RtAgBfoy`+O_S~CCu delta 1205 zcmV;m1WNnAF}X3YzXFr@0xpy70t%DA0xq*90|60#nK6d$gAM342i4T6I3r5^0@?{} z(obm9P#g#FXO^z9)bOY!loGNSk?j)Grt4q>VVkqanC!oL7?#)Mbt$$@O;V?_!w!lQ z#ws#Ah^K)&?4V;Og@L0E4}*aW4?F0F9;V>Qq1!}3^D-D#{Dt=+Jo5YTec@5m>i*~H z>Am@XncGJI026iDTpqV6iZX0t%T`L3naN-aYldShUHCfAn1j)kuXOND2ZD4)3bE_EX_RH*zpIxRW|E`509~{K{(|Pz4^IGhZlY<(A@Id zKxE?gT{^n*>_r{F#0rZ(@qtg;@d0-PnMM=p2@D}X2ohKVB!mcI0!N4tq6D5G5JZB1 zL?Cm)Uqe9v1OW@+uPqqbe|aM8E^g}e9u;w4qVD2>aDRK75(yFkxR3Dfxf!^;@Yrem zhB73qm6>n8lNlKZPQ!XO0>(!EbH}bmq->_iu z-ENZ?wosuC!b00HO~))dY{l)AvNOzlxW#pH%t&RXBLeSy>3=S~*V+$L zw)6a9+c=>%V?6F=OnD`VgM$~vZe$e20X&L}wgv#jFdO%t9Q`==_4vxxyx``4`2);v z1er00?t=~JH3!wysW>A_{Q}?zZIHoh(@-48e|gT4g%TbmloIN!1?djPbV)m$AarF; z9m-U;xr12JA8u{^dew`6H~`o@VY^LsQB~EYfcb&i@x4Za`OI{}K%Hd|Skvn; zUk!c3Tyxv2b;}5iy6Lq;!)@a^duX&eO%|xG6$GrlZurdcx&b>DlBOCjEh99Yv)Y_% zU7DS{uH2YeROUxC zj^W?$vQWG^bNklPBk%$|OIKH>PE_`}_E%lvyx5<`1wq3myAbTKH>`aJ?LV}2r`QBh=8OJ5ur#Lk&q0MMPww0j3arZ zfDB^bI0OVpfd~}RAVI8ufeblF3P3XdK8L4!Z=Qw4gOeaoT7-B~lI{9ISa*!3Eynj3 zYV*qA5A_wwJ6?;Wd0k)bOT6%Lte>Sr3pQnXG;w#k|MT=-E}yhEoc~tC?w6y9xVp7X zhSfB1JTLURVWL>Ou>7|e;9gDQUtbxkW$Du$oat&+9`1HdRTIS=Xl=LvgrEd}0Q7jr TnK6d$gAM342i4T6I3r5^#nDvE diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-377391038018914117-0-d962db46-093c-45e8-be4a-0c6d572dad92.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-377391038018914117-0-d962db46-093c-45e8-be4a-0c6d572dad92.avro deleted file mode 100644 index 381c63dab7cc18bfda7f96f2b2a4583cf31fa0da..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1851 zcmb7EO-K}B7&Zg*5dDBzZeiQ76@jZWuC}W|bdnTt%Ziqj8o!)HTj-EP zha%R+cGrz1aD_q>k4Z=bi9zA1?m>mxLFiJ)zS-SbM@L7ye&72%&-*^_k3ZDEXMeRO z0YwYT$j!4ZkJsyIp-7L9qIBt{uL8d7bMknr+8Gvv$&KuqB z&LJL!oo&~gd3{L4>YQ6>2vF+VLWMG^2r|~#fW{^vUe}>D1u`_w%qYdkveAU+CA1OF z$P&UPZw66bJCM-;y3ha%xY-1@W-RhJ*J2B|V>oZi09Y2Xv@sW3)$G{?p5q0m(Tp6i zmQOaB@D|VDU0y@D*|Z&l*#ip$3k8U933dc#3oHZ$2UK}zn*u0v)2%HiRbazlZ=q3v z4T0H1YYL^g)i#e0X_~zrLf?2CvA(cCRY6q8U=Kz8J`|VHho5)^MSX+UqwUvQpMJng z8Xm*EO5p|=j4h@i2+6Vlfn;5t0v!L~;QR$CqA5L};&CWdK^Qc*@FzY@AR-`~uCBge zsRelms&ojCre&Qe)6p^UMiJDA9atlZvMsCW$n1hu7cx$y+D!V0SZoLF0T`u4hyV)^ zuvld>`ai`>_gUX0B3Aa|;ryWyKR5lZ_022a(%ZiB`+G0EPmHBT`*SlhCxe;b^(p#F zBAZAA6Y0#=?y0qSa4bDaKMhXM-HBx8dFokhHXeu{%Vx4S+KD#ria-NA4d=VQD#Xp^Y{0o8rMSEgT{L4;@td`Q**Z~ U7QW3NUcOgRQTDy9b8|`m01RAb1poj5 diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4114664439361220711-0-0e34766b-2d66-44ac-b491-723a4772a1a3.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4114664439361220711-0-0e34766b-2d66-44ac-b491-723a4772a1a3.avro new file mode 100644 index 0000000000000000000000000000000000000000..5551fd226862a95fd7a8f0ad0f46ef3e84897ef5 GIT binary patch literal 1936 zcmb7ETSyd97`BUqDTZm=O=25DiQL-Rt7cI_*}Y7uM9J|ubJm@l%QkaXTWU))i=uq! z>O%=7775*{o?+2WhdGog}%;10*OQ)3`9J&7Nj)+8t@BN{rcRbu5wLAukkY zXcD}9A(W$Ur)lfi4apXb3*Zv>WoEe*O^x`8<$V zZ6qW}ixNYm7zq~(IY!2Bc}EBV@WEdm2&@ari1GmB4h7--ahTm~#we9XMiMI&DIttV zE5tiQ8X%mFac|aEV=0T=#*A!ZP#+Th6yqcs92ANsQ6V)doQO5Xp|NL(RdguX18E9p zrk0{+Y0=#qJXgH6G5ai97t^dHPQeb*scNVGv-;G>u3w7VsM0I01N{eN*$BQ zYPS9Z-oo*Ov#D?lS`Ty#4CElfC4?i;TA(8saX=DFrzn8b zHeKI>Pz7Na!ds|T5QaeOp+1Gs+{zQj$E=yXJVNkt9F7=USRhHjr(m#$ymBr^moXJT zu?X_Y29HIDUvFjl0V~LO41+2oH$Y`f(+okOC~^=8`sEpc;~yLxyda}!QpsoZIE1Po zRGI_$6CWnv_y0w^82Tt4BESFybXIAM{!j7HeO5M!@JE0x$Lr#&-=AAM=#J=0Zc3kdc4lkG zlWi^Q6C!)Jx6{M3`lC84tyRWDe9-Rt|iL zHr#xhv7xy&UywV>vyWO=J&*40y*8BISmkWmHMfz;xYy@jl)cBv_=-Cd``>h$TMegg z)#tX~t-l?0W#@*f{yVa5&8F?O-&;C6dcR({`a19~K6BZFtf=Vhy6Q{r9D;5}3vRq# zy|FxQMTI@F=X-a=@b6y(JwIK?e86A!wZCm%L`TKoz8`t%-2F#wO%)#t_NNq;b*+o5 zjcM3?{Y*ksQeS36PUML*HC5g#Cr`co;QajTsPv-7@aogI0C*WUlu!LmijBQ$eLCyQ J`Ra+j`~isFka_?B literal 0 HcmV?d00001 diff --git a/debug_warehouse/tracking/tracking_data/metadata/snap-6099500306663132840-0-e525fa0c-bd68-4ba7-bfc2-9a6c8f662a6d.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4345927569820710336-0-3faa5e91-e6e1-4926-9601-15797fb2ce84.avro similarity index 82% rename from debug_warehouse/tracking/tracking_data/metadata/snap-6099500306663132840-0-e525fa0c-bd68-4ba7-bfc2-9a6c8f662a6d.avro rename to examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4345927569820710336-0-3faa5e91-e6e1-4926-9601-15797fb2ce84.avro index b07c799e0766df7977b9f9b082d5ee09dc35fb6a..d935c07d1c0b8d31f504ce90fcee56cce24f8dba 100644 GIT binary patch delta 275 zcmZ3=w}(&FKPiimN31w6v7k63zeG1PMa{(6#MIKr+|ig?> zojswi>(`p?aCPPU*-?`?(j=Cvs?Ay!CG~3AtdylvuVzI^&3qX(OH$&M)T~+2vzDt$ zsxFV3@#2}(3)N|&PtSX&B_t$e?f6&q_0_iP&$S~YB>ZZA9^ny6W^j@eH($dtxz}Udd%*@!(*vP`fV56!DTm5N? z7p&5?^Vi_PlrTjeOWuyzlD^oBX*`g=AMLRqQ(^r+s_T!whPSFeB} w$JtjITzfb7^D0icckcW1r@yRZ+^0DDw*>uUC|v609)U1bN~PV diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4761525111313210917-0-4665052f-9f33-4c4a-b55d-e1fd2bcb223d.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4761525111313210917-0-4665052f-9f33-4c4a-b55d-e1fd2bcb223d.avro deleted file mode 100644 index e0260358df2d37308f6c03350f9f460b985304cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2000 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 literal 0 HcmV?d00001 diff --git a/profile_warehouse/catalog.db b/profile_warehouse/catalog.db deleted file mode 100644 index e860b6af3fc5938907b200651b7537c28407c693..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20480 zcmeI&&rj1}7zgln{3ft@&=5sTjV7>!vX-@5rw5(5L?ePu&=5kJx7|~yj1}7s;ks}z z{z>?sa4=pxbM@%E&24PDNk~+P-=<6Zw$GdQ$>(Vg@7uk4x#)%@IiA16LMbU+5kygV zC`p1K4DsIp|24JYZ?4!k;)GJb&86B}%$ckQl6zO#~zwjiRpPAS7xU{sqA}y^hF3w7$Bh4JThx|zH zz}%*TGeqsNVwsw8s-h_EBfiK#eV)y)NzZa?(#+{3XQgH`v(o87C6{eu(RIZ<#+ z1=rrPW7KxIK3nuy26!)#k=YK>+w>aEBx`Rn)24ql6ImA(n} zQ)OC#-D7qrWCdPlv*>Y2*DEzcYwpzo@A|aodgZ`8Lu`-Iw6du^v9nU^0x!?~$l%0s z{CDN4bqQJpski(1J2hN=ug0SVAu@I%jGaV3obNqDF9<*Y0uX=z1Rwwb2tWV=7gC_& zi^G#s)0OqgdVw5PE{37c>}|KS**Kcjsh#5%>0Tm{SPjSz5`0>0@wEf8x56OtnD(ex zT{uXz^)PL(6f(EOuk-L~fAc-_X^T$+sTtWksST27Ob!!#x~`~l%2BKgIgU(LG9_yT zGGs&5(z4AgR?rN~W+`K?(P!>$;FbPCnu=^CS&A`>6NDMb@2Jfv zFHR(OB*czlpD*Mai=-d`0SG_<0uX=z1Rwwb2tWV=|FyuK0lr=$_FOE9+~zUUBONhq z>sI63P>g*M_>K(%5P$##AOHafKmY;|fB*y_0D(&?a7*M%0N1Z|ECJBI8-zjVm#O;w rUyOYf_>K(%5P$##AOHafKmY;|fB*y_0D;RaaHF~g(D^rj`WnD@(OO;~ diff --git a/profile_warehouse/tracking/match_metadata/metadata/00000-4a6aa9a1-65e0-450f-9be7-7684faeeac52.metadata.json b/profile_warehouse/tracking/match_metadata/metadata/00000-4a6aa9a1-65e0-450f-9be7-7684faeeac52.metadata.json deleted file mode 100644 index 94ab754..0000000 --- a/profile_warehouse/tracking/match_metadata/metadata/00000-4a6aa9a1-65e0-450f-9be7-7684faeeac52.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/profile_warehouse/tracking/match_metadata","table-uuid":"86f5fe78-2067-4062-8790-9b34dfb2a728","last-updated-ms":1775647690681,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":true},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"snapshots":[],"snapshot-log":[],"metadata-log":[],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":0} diff --git a/profile_warehouse/tracking/match_metadata/metadata/00001-9b21cef5-89c7-436e-83d6-51ccb7009828.metadata.json b/profile_warehouse/tracking/match_metadata/metadata/00001-9b21cef5-89c7-436e-83d6-51ccb7009828.metadata.json deleted file mode 100644 index 7a28cf5..0000000 --- a/profile_warehouse/tracking/match_metadata/metadata/00001-9b21cef5-89c7-436e-83d6-51ccb7009828.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/profile_warehouse/tracking/match_metadata","table-uuid":"86f5fe78-2067-4062-8790-9b34dfb2a728","last-updated-ms":1775647694517,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":true},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"current-snapshot-id":2219873109471251602,"snapshots":[{"snapshot-id":2219873109471251602,"sequence-number":1,"timestamp-ms":1775647694517,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/profile_warehouse/tracking/match_metadata/metadata/snap-2219873109471251602-0-e27f5ba5-48f7-4cb2-ae56-16a5fe7fd851.avro","summary":{"operation":"append","added-files-size":"1525","added-data-files":"1","added-records":"1","total-data-files":"1","total-delete-files":"0","total-records":"1","total-files-size":"1525","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0}],"snapshot-log":[{"snapshot-id":2219873109471251602,"timestamp-ms":1775647694517}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/profile_warehouse/tracking/match_metadata/metadata/00000-4a6aa9a1-65e0-450f-9be7-7684faeeac52.metadata.json","timestamp-ms":1775647690681}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":2219873109471251602,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":1} diff --git a/profile_warehouse/tracking/match_metadata/metadata/snap-2219873109471251602-0-e27f5ba5-48f7-4cb2-ae56-16a5fe7fd851.avro b/profile_warehouse/tracking/match_metadata/metadata/snap-2219873109471251602-0-e27f5ba5-48f7-4cb2-ae56-16a5fe7fd851.avro deleted file mode 100644 index 89a2da13a9463b978bd66b33e260329ecbec8a09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1819 zcmb7ETSyd97|w-4DopUwgBO;e76P|sR#$g}2q7ZF(2^cXVmLc<);&2hXPR?HU2Khl zB8Ujo7qKXVAdHGaqoRwFkv&O54@rRq^{OHHR&-`}XB{0K>-C)f`@aAG{!3Hrbl%od zq5+*GFbyI(5DE#=NL{tSM{DYYP*|wtLpyX}K#c|+VfmV=D3t`BF`*{Gpk}HuX!wNk zI5t#3gB{Qy2y4DjG3YSxZVAg!Dv)7Z0Th;|pNN!zDtNHHo1;k`a(n%p0VQn6T$P`T zBdExPy|s388c@@}D$r0I5-RF|CUQY6VHOtVd4{ri2sMz_W`)RDvWOe81SreKfBaa3 zzZ21f|6q$htq-|Wm46it1!`@ps0a=f1v8ChXl4>cmX1htD2XgHyA(UikOPl2x*RTH zjWUy0f`mFRklg?=(*PGZ>;StnRuRiJ+rqUNYCRc%EJIne=VG;*tN#HXM+zjOgiUw( ze2oL|@(gw%LRqs#F9xRvE(RGYkg^iI2%HwU2r>>BxO0sH1ZUITEyz{i!QgG7U4aLI z(?fR(xw$1P$A@>!UMpv9Jd0RbSYQ|+X{zJQ+CqVZrGOl0^$vKH1}ttAev>PO(pRn1HEJmae6~Y^fF8 z2@NsEM$@&wX@vO*mrAXp-42tY3Mr5Q1um-` zM*pXH?mk^O9XZ!qvZ;S&F>kQvPutm_-%Bb!E`CEhhu++3|8#Wh?fKE>-m;a9E)?dTl$GE3U3vTRqj%)_7x5;V`tfUOTja(5l^Op9Ts2xr diff --git a/profile_warehouse/tracking/tracking_data/metadata/00000-b3a6aabe-7045-4467-ab11-81db388158b2.metadata.json b/profile_warehouse/tracking/tracking_data/metadata/00000-b3a6aabe-7045-4467-ab11-81db388158b2.metadata.json deleted file mode 100644 index 7453fe5..0000000 --- a/profile_warehouse/tracking/tracking_data/metadata/00000-b3a6aabe-7045-4467-ab11-81db388158b2.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/profile_warehouse/tracking/tracking_data","table-uuid":"e3737ab8-990e-4a3d-9c9d-23e2292ada10","last-updated-ms":1775647690678,"last-column-id":11,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[{"source-id":1,"field-id":1000,"transform":"identity","name":"match/id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"snapshots":[],"snapshot-log":[],"metadata-log":[],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":0} diff --git a/profile_warehouse/tracking/tracking_data/metadata/00001-6f0b9eff-eb36-4de8-8147-cabad48bca68.metadata.json b/profile_warehouse/tracking/tracking_data/metadata/00001-6f0b9eff-eb36-4de8-8147-cabad48bca68.metadata.json deleted file mode 100644 index 1c440ba..0000000 --- a/profile_warehouse/tracking/tracking_data/metadata/00001-6f0b9eff-eb36-4de8-8147-cabad48bca68.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/profile_warehouse/tracking/tracking_data","table-uuid":"e3737ab8-990e-4a3d-9c9d-23e2292ada10","last-updated-ms":1775647694509,"last-column-id":13,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false},{"id":12,"name":"name","type":"string","required":false},{"id":13,"name":"entity_key","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":1,"field-id":1000,"transform":"identity","name":"match/id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":3010651843225263066,"snapshots":[{"snapshot-id":3010651843225263066,"sequence-number":1,"timestamp-ms":1775647694509,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/profile_warehouse/tracking/tracking_data/metadata/snap-3010651843225263066-0-f03266b5-4cd0-499d-b61d-580fce440e8d.avro","summary":{"operation":"append","added-files-size":"69406005","added-data-files":"1","added-records":"3650000","changed-partition-count":"1","total-data-files":"1","total-delete-files":"0","total-records":"3650000","total-files-size":"69406005","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":3010651843225263066,"timestamp-ms":1775647694509}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/profile_warehouse/tracking/tracking_data/metadata/00000-b3a6aabe-7045-4467-ab11-81db388158b2.metadata.json","timestamp-ms":1775647690678}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":3010651843225263066,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":1} diff --git a/profile_warehouse/tracking/tracking_data/metadata/f03266b5-4cd0-499d-b61d-580fce440e8d-m0.avro b/profile_warehouse/tracking/tracking_data/metadata/f03266b5-4cd0-499d-b61d-580fce440e8d-m0.avro deleted file mode 100644 index e64c3fc0fa37b43a91ca4a668abfb754f09740e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5402 zcmb_gYitx%6b9PbidrEOELh`ZM0vQ|?#x18YJvtUEstX5jdi^{bEiA)?96uNZecYA zu|6sqDWE(ALsLZ!KTNC9_()8xHNI+nC1{8-`~ZK55+o!7M)cg7&g{;%)V6NYH0|u1 z$NkQCzH{!@gc~O2Uu}vJ62*^Q^9V;1F~U`FCN<(B<$Rp15LGg{ij9wOio{j$K2F0? z_C^#_F;WcAd?_}HRuBIhq$#c#1YVc1YLXV8Z7aC8q8^PAs!&DO#^fRBdN2t#$7sC3 zHKt0Gm=s52$=p*PT_*j7uDxsk1fkTUtNKU>FL5nn#8C7xKvtTxRB?hBV?wLUHKqhd z$7EXWnr=>FB&#}Rl(-B&e84qIjNT|osE0>ylw^`c#!)knPZEe|z`sPOk)-4bq#vHZ zbw1AS5dUBgC=#=W-Z1=DgTQmvBXuo>F>Khx_M0&xK0guq#?<46NE}n=edwh?03f3V z)=XJ9qR>;eTgp;O@3NLU&g&ZdO$rn;Lrne6#4w?mCJEDUv!NG>xY~wPpzY%0K9Cg1>zKqaxFfT zVxi%f8PTcLC4)|FGd^jyk<;R;%G_H0QwM)Q&N!}8t^%=-ye>gih$fPd7LSIBk(Nzx zyl!PhB_VS}-wVd;n;{-z0n(Bck`6~(g25uDIpTvUxmi_U80sS6$5ldHWb*^DLRKJF zdmqvbk^MqICjd09ZWbCbOe5)mfU3!IJB{MTTBeJbsX1C*TZJ|$G=eJcM;6P7mlu|Q zJEj%}s z>8s=n9m0HOO{3VKuhZ2r)JCmcnlN zkf_I@dBJ&7m=GUGX(zimlhIdL?T2(3*&a-!*uhwBy&o65s5EVWQLK}ouX@PSnR z+Edg$<|>_H6)_)(`CwseA0Al{bu}K^c(cc z5wxTpIz?#Y`O;7`&zHGaF^d$Dv_^>=&S}O}RQXa*=FM1*Tgu=BbKz{tQ&i&;=#@3J5Y{BL0H24QnHU87$2tn87jtGgv-;FoR{D>_^r3!SYOk z$u=gY2f#}kRadobh48|w>YIoW3Ip!2)&i2Lf+Q@=B(y>a<7ufIWXzf>l#C4Ruv14U z%^bECR>;E23t~J5x4p}On9;!`Ff|SS4>cA3LrsSTF_=L)wXhlvL>Q`}D501wIxa~D zFA~9&h0&evoSkbOko0n+@g7Yy5;1FC1^38tkHV@R_Lk6$sR|A0vTPDMq!EHCJER*v zxk0$g1yM;9u3t{lY_oFq4STg3agN{5QlK~tOOTU?hwyN5!SF@`E?Pq~9A&GfWmqz{ z3@h0;M6h9)D9$!d*fS(Ti4j#af((5Vax!H5oBOu}F)3V6V7W7k(m>Y@z}%3ad4_J< z3x=DL)~+>D#j6&>t{O9l$!_g1rcDR|S0D(%E=aH`E2fo-y;3b!L8PfBu(S8s2~5Xl>44)VLd#AtFySlUC+uZBkd-|5oXB?^2_Ro9gy(ceTfjSyH z4#*FwlDtPc(zxqjxT7(w?C(4v$@@AR8*uxcgAL(ud&ADowe9Oxt$T4p@AbzErWO1; zv$tLJJXO;C##VF7TW9;jCqMXTR@dXpBdh;f zf7i|%ifgXgexq{d&2zu~q$u9@%*5AoUwJ8Kao4uI{3##haGMup-_$l`!qmIb-Pw!V zw&h(pWmXQiNDiPGJw2cFeBARX>N$I$_>#}wieAZn!>FSj|IrV=8 zW}NMwJ#%tw_MSOwCO7>ybwgs&;~!u8{jscY;XiYV?nJZJwC`TGf5Wsxd-olfe@lBL zvNEeSuk=h$b9X^y*S1S*mgQwV^KAC{?X}-O+O%(eU-#)BAIP2h+@BZjE$l1%x%j~o rk2buzWaoX?teW@D;lje(k2O#BAG^5z@TtEym>o?MZaw{GdNKb6lug*6 diff --git a/profile_warehouse/tracking/tracking_data/metadata/snap-3010651843225263066-0-f03266b5-4cd0-499d-b61d-580fce440e8d.avro b/profile_warehouse/tracking/tracking_data/metadata/snap-3010651843225263066-0-f03266b5-4cd0-499d-b61d-580fce440e8d.avro deleted file mode 100644 index da5df86c17c36626620d8207c644478ec89c2e4f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1829 zcmb7EO-K}B7}kwQETtc@gpzTPh}g}U{mb;0k(7a?8HnWc?aa6C)ciT$H*SkAQ3w$p zB0J?FLNx4<)lIy)f@D%q_ID^L3h9t^@nQ$-o86stbab@q_r1^ayzle=G)AuE?!Q+Oh6pNe5}#9ffJrt~OVb%D zF5(zs{vf50S<7;26kpFizL@<3pZj2yD|VlfC6XD#dNjaF_g^>UWs;JZ*%&As30at7&xr5 z8U3H)x%+Hx5}nLj&aYkS55Ang^=sSQbXCzq&s$GRUqfMvInZ0TSi?SkG;Aysy)&l2 zKlLStDxbGcG!&E%j-fBZ^_>eRpBWV|7^bHOO}3t&7-$-gUp^#1xx3dp^0{{WM)A?v zo5OR3Ev2t2-lzI5+@Iu*{X6wqFs diff --git a/python/tests/test_catalog.py b/python/tests/test_catalog.py index 28bbf8e..4deebea 100644 --- a/python/tests/test_catalog.py +++ b/python/tests/test_catalog.py @@ -31,9 +31,10 @@ def test_catalog_lifecycle(tmp_path): # Check data df = loaded.to_polars() assert df["match/id"][0] == "match_1" - assert df["ball/player_id"][0] == "7" + # ball/player_id was an input, but after melt/pivot it becomes ball/player/id + # as player/id is a core attribute. + assert "ball/player/id" in df.columns assert df["ball/x"][0] == 0.0 - assert df["ball/x"][1] == 1.0 def test_bound_container_save(tmp_path): diff --git a/python/tracking_data_container/catalog.py b/python/tracking_data_container/catalog.py index bbf9118..c0b07aa 100644 --- a/python/tracking_data_container/catalog.py +++ b/python/tracking_data_container/catalog.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import logging from collections.abc import Sequence from pathlib import Path from typing import TYPE_CHECKING, Any, cast @@ -38,6 +39,20 @@ if TYPE_CHECKING: pass +logger = logging.getLogger(__name__) + +# Mapping from internal CDF-like names to snake_case Iceberg names +COLUMN_MAPPING = { + "match/id": "match_id", + "competition/id": "competition_id", + "season/id": "season_id", + "player/id": "player_id", + "team/id": "team_id", +} + +# Reverse mapping +REVERSE_COLUMN_MAPPING = {v: k for k, v in COLUMN_MAPPING.items()} + class TrackingDataCatalog: """ @@ -128,26 +143,26 @@ def _create_tracking_table(self) -> None: except NoSuchTableError: pass # Create new table - # Define the requested schema with flat names. All fields are optional to allow for - # flexible ingestion and schema evolution. + # Define the requested schema with snake_case names and required fields. schema = Schema( - NestedField(1, "match/id", StringType(), required=False), - NestedField(2, "competition/id", StringType(), required=False), - NestedField(3, "season/id", StringType(), required=False), + NestedField(1, "match_id", StringType(), required=True), + NestedField(2, "competition_id", StringType(), required=False), + NestedField(3, "season_id", StringType(), required=False), NestedField(4, "timestamp", TimestampType(), required=False), NestedField(5, "frame_id", LongType(), required=False), NestedField(6, "period", StringType(), required=False), - NestedField(7, "player/id", StringType(), required=False), - NestedField(8, "team/id", StringType(), required=False), - NestedField(9, "x", FloatType(), required=False), - NestedField(10, "y", FloatType(), required=False), - NestedField(11, "z", FloatType(), required=False), + NestedField(7, "entity_type", StringType(), required=True), + NestedField(8, "player_id", StringType(), required=False), + NestedField(9, "team_id", StringType(), required=False), + NestedField(10, "x", FloatType(), required=False), + NestedField(11, "y", FloatType(), required=False), + NestedField(12, "z", FloatType(), required=False), ) - # Define partitioning by match/id + # Define partitioning by match_id partition_spec = PartitionSpec( PartitionField( - source_id=1, field_id=1000, transform=IdentityTransform(), name="match/id" + source_id=1, field_id=1000, transform=IdentityTransform(), name="match_id" ) ) @@ -171,9 +186,9 @@ def _create_metadata_table(self) -> None: # Define schema for metadata as requested schema = Schema( - NestedField(1, "match/id", StringType(), required=True), - NestedField(2, "competition/id", StringType(), required=False), - NestedField(3, "season/id", StringType(), required=False), + NestedField(1, "match_id", StringType(), required=True), + NestedField(2, "competition_id", StringType(), required=False), + NestedField(3, "season_id", StringType(), required=False), NestedField(4, "metadata_json", StringType(), required=True), ) @@ -253,10 +268,44 @@ def _save_tracking_data( assert self._tracking_table is not None - # 1. Melt to entity-long format for stable warehouse storage + # 1. Melt to entity-long format long_df = container.melt() - # 2. Convert to Arrow and handle dictionary casting + # 2. Add entity_type and rename columns to snake_case + if "entity_key" in long_df.columns: + long_df = long_df.with_columns( + pl.col("entity_key") + .map_elements( + lambda k: TrackedEntity.from_entity_key(k).type, return_dtype=pl.String + ) + .alias("entity_type") + ) + else: + # Fallback if entity_key is missing (shouldn't happen with container.melt()) + raise ValueError("entity_key column is required for saving to catalog") + + # Handle potential name collisions before renaming + # If any target names already exist, drop them + to_drop = [ + v for k, v in COLUMN_MAPPING.items() if k in long_df.columns and v in long_df.columns + ] + if to_drop: + long_df = long_df.drop(to_drop) + + # Rename columns to Iceberg format + rename_dict = {k: v for k, v in COLUMN_MAPPING.items() if k in long_df.columns} + long_df = long_df.rename(rename_dict) + + # Ensure match_id is present and correct + long_df = long_df.with_columns(pl.lit(match_id).alias("match_id")) + + # Check required fields + if long_df["match_id"].null_count() > 0: + raise ValueError("match_id cannot be null") + if long_df["entity_type"].null_count() > 0: + raise ValueError("entity_type cannot be null") + + # 3. Convert to Arrow and handle dictionary casting arrow_table = long_df.to_arrow() # Define known types for core columns to avoid incorrect casting when null @@ -298,7 +347,7 @@ def _save_tracking_data( txn.update_schema().union_by_name(container_schema).commit() # Delete existing match partition - txn.delete(EqualTo(term=Reference("match/id"), value=match_id)) # type: ignore + txn.delete(EqualTo(term=Reference("match_id"), value=match_id)) # type: ignore # Append long-format data txn.append(arrow_table) @@ -313,9 +362,9 @@ def _save_metadata(self, metadata: dict, match_id: str) -> None: # Define schema for the Arrow table metadata_schema = pa.schema( [ - pa.field("match/id", pa.string(), nullable=False), - pa.field("competition/id", pa.string(), nullable=True), - pa.field("season/id", pa.string(), nullable=True), + pa.field("match_id", pa.string(), nullable=False), + pa.field("competition_id", pa.string(), nullable=True), + pa.field("season_id", pa.string(), nullable=True), pa.field("metadata_json", pa.string(), nullable=False), ] ) @@ -323,9 +372,9 @@ def _save_metadata(self, metadata: dict, match_id: str) -> None: # Prepare metadata row metadata_row = pa.table( { - "match/id": [match_id], - "competition/id": [metadata.get("competition_id")], - "season/id": [metadata.get("season_id")], + "match_id": [match_id], + "competition_id": [metadata.get("competition_id")], + "season_id": [metadata.get("season_id")], "metadata_json": [json.dumps(metadata)], }, schema=metadata_schema, @@ -333,7 +382,7 @@ def _save_metadata(self, metadata: dict, match_id: str) -> None: # Upsert metadata with self._metadata_table.transaction() as txn: - txn.delete(EqualTo(term=Reference("match/id"), value=match_id)) # type: ignore + txn.delete(EqualTo(term=Reference("match_id"), value=match_id)) # type: ignore txn.append(metadata_row) def __repr__(self) -> str: @@ -382,16 +431,16 @@ def schema(self) -> Fields: assert self._catalog._tracking_table is not None - filter_expr: BooleanExpression = EqualTo(term=Reference("match/id"), value=self._match_id) # type: ignore + filter_expr: BooleanExpression = EqualTo(term=Reference("match_id"), value=self._match_id) # type: ignore if self._player_id: filter_expr = And( filter_expr, - EqualTo(term=Reference("player/id"), value=self._player_id), # type: ignore + EqualTo(term=Reference("player_id"), value=self._player_id), # type: ignore ) - # Only scan player/id and team/id to reconstruct schema + # Only scan player_id and team_id to reconstruct schema entities_table = self._catalog._tracking_table.scan( - row_filter=filter_expr, selected_fields=("player/id", "team/id") + row_filter=filter_expr, selected_fields=("player_id", "team_id") ).to_arrow() entities_df = cast(pl.DataFrame, pl.from_arrow(entities_table)) @@ -401,21 +450,40 @@ def schema(self) -> Fields: # All potential physical attributes (x, y, z, etc.) physical_names = [f.name for f in self._catalog._tracking_table.schema().fields] - attr_cols = [c for c in physical_names if c not in CORE_COLUMNS and c != "id"] + # Ignore core and metadata columns + ignored = set(COLUMN_MAPPING.values()) | { + "entity_type", + "timestamp", + "frame_id", + "period", + "period_id", + "entity_key", + } + # Anything else is an attribute (like x, y, z, player_id, team_id, etc.) + attr_cols = [c for c in physical_names if c not in ignored and c != "id"] + + # Base list: all core columns that are NOT entity-specific (internal names) + # We use COLUMN_MAPPING to check if they exist physically + wide_names = [] + for c in CORE_COLUMNS: + if c in ("player/id", "team/id", "entity_key"): + continue + phys_name = COLUMN_MAPPING.get(c, c) + if phys_name in physical_names: + wide_names.append(c) - # Base list: all core columns that are NOT entity-specific - wide_names = [ - c - for c in physical_names - if c in CORE_COLUMNS and c not in ("player/id", "team/id", "entity_key") - ] for row in entities_df.to_dicts(): - p_id = str(row.get("player/id")) - t_id = str(row.get("team/id", "none")) + p_id = str(row.get("player_id")) + t_id = str(row.get("team_id", "none")) prefix = TrackedEntity.from_entity_key(f"{p_id}|{t_id}").cdf_prefix() for attr in attr_cols: wide_names.append(f"{prefix}{attr}") + # Also add player/id and team/id as attributes for this entity if they exist physically + if "player_id" in physical_names: + wide_names.append(f"{prefix}player/id") + if "team_id" in physical_names: + wide_names.append(f"{prefix}team/id") self._cached_schema = Fields(list(dict.fromkeys(wide_names))) return self._cached_schema @@ -455,11 +523,11 @@ def collect(self) -> TrackingDataContainer: assert self._catalog._tracking_table is not None - filter_expr: BooleanExpression = EqualTo(term=Reference("match/id"), value=self._match_id) # type: ignore + filter_expr: BooleanExpression = EqualTo(term=Reference("match_id"), value=self._match_id) # type: ignore if self._player_id: filter_expr = And( filter_expr, - EqualTo(term=Reference("player/id"), value=self._player_id), # type: ignore + EqualTo(term=Reference("player_id"), value=self._player_id), # type: ignore ) # 1. Determine which long-format columns we need @@ -485,21 +553,42 @@ def collect(self) -> TrackingDataContainer: literal_names = list(dict.fromkeys(literal_names)) deferred_select = literal_names - needed_long = set(CORE_COLUMNS) + needed_long_internal = set(CORE_COLUMNS) needed_ents: set[str] = set() for name in literal_names: if name in CORE_COLUMNS: - needed_long.add(name) + needed_long_internal.add(name) else: field = DataField(name) - needed_long.add(field.attribute) + needed_long_internal.add(field.attribute) if field.entity: needed_ents.add(cast(str, field.entity_key())) - # Check physical schema + # Check physical schema and map names physical_names = {f.name for f in self._catalog._tracking_table.schema().fields} - selected_long_fields = [f for f in needed_long if f in physical_names] + selected_long_fields_set = set() + for f in needed_long_internal: + phys_name = COLUMN_MAPPING.get(f, f) + if phys_name in physical_names: + selected_long_fields_set.add(phys_name) + + # Always include required fields for pivot + for req in [ + "match_id", + "entity_type", + "player_id", + "team_id", + "frame_id", + "timestamp", + ]: + if req in physical_names: + selected_long_fields_set.add(req) + + # Include any other physical column that might be an attribute + # (If we selected an attribute like 'x', it was already added above via field.attribute) + + selected_long_fields = list(selected_long_fields_set) # 2. Add entity filter pushdown if needed_ents: @@ -507,9 +596,9 @@ def collect(self) -> TrackingDataContainer: for k in needed_ents: p_id, t_id = k.split("|") - cond: BooleanExpression = EqualTo(term=Reference("player/id"), value=p_id) # type: ignore + cond: BooleanExpression = EqualTo(term=Reference("player_id"), value=p_id) # type: ignore if p_id != "ball" and t_id != "none": - cond = And(cond, EqualTo(term=Reference("team/id"), value=t_id)) # type: ignore + cond = And(cond, EqualTo(term=Reference("team_id"), value=t_id)) # type: ignore if entity_predicate is None: entity_predicate = cond @@ -530,6 +619,32 @@ def collect(self) -> TrackingDataContainer: scan = self._catalog._tracking_table.scan(**scan_kwargs) arrow_table = scan.to_arrow() + # Rename back to CDF-like names for Rust pivot + # Handle potential collisions here too + rename_map = {v: k for k, v in COLUMN_MAPPING.items() if v in arrow_table.column_names} + if rename_map: + new_fields = [] + new_columns = [] + for field in arrow_table.schema: + if field.name in rename_map: + target_name = rename_map[field.name] + # If target name already exists, we prefer the one being renamed + if target_name in arrow_table.column_names: + # Skip this column, we'll use the renamed one later + continue + new_fields.append(pa.field(target_name, field.type, field.nullable)) + new_columns.append(arrow_table.column(field.name)) + else: + # Keep original name + if field.name in rename_map.values(): + # This name is a target of another rename, and we already handled it + # above (by skipping it if it was a source, but here it's an existing + # column with target name). We skip it here to avoid duplicates. + continue + new_fields.append(field) + new_columns.append(arrow_table.column(field.name)) + arrow_table = pa.Table.from_arrays(new_columns, schema=pa.schema(new_fields)) + # 4. Pivot to wide format using high-performance Rust implementation wide_container = TrackingDataContainer.pivot(arrow_table) wide_container._catalog = self._catalog From e655624b1f1c8a109e4bca2a07d3480a87122b54 Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Wed, 8 Apr 2026 14:05:01 +0200 Subject: [PATCH 06/21] feat: rename player_id to entity_id and update iceberg field order/requirements --- crates/tdc-core/src/datacontainer/core.rs | 29 +++++- python/tests/test_advanced_features.py | 2 + python/tests/test_catalog.py | 16 +-- python/tracking_data_container/catalog.py | 102 +++++++++----------- python/tracking_data_container/container.py | 7 +- 5 files changed, 87 insertions(+), 69 deletions(-) diff --git a/crates/tdc-core/src/datacontainer/core.rs b/crates/tdc-core/src/datacontainer/core.rs index 10c9970..b8baebd 100644 --- a/crates/tdc-core/src/datacontainer/core.rs +++ b/crates/tdc-core/src/datacontainer/core.rs @@ -419,7 +419,7 @@ impl TrackingDataContainer { } else if name == "timestamp" { timestamp_idx = Some(i); core_indices.push(i); - } else if name == "player/id" || name == "player_id" { + } else if name == "player/id" || name == "player_id" || name == "entity_id" { player_id_idx = Some(i); attr_indices.push(i); // Also pivot it } else if name == "team/id" || name == "team_id" { @@ -435,7 +435,26 @@ impl TrackingDataContainer { } } - // 2. Select the best frame index column + // 2. Select the best frame index column and validate required fields + let has_x = fields.iter().any(|f| f.name() == "x"); + let has_y = fields.iter().any(|f| f.name() == "y"); + let has_period = fields + .iter() + .any(|f| f.name() == "period" || f.name() == "period_id"); + + if !has_x || !has_y { + return Err(TrackingDataContainerErrorKind::InvalidParameters( + "Missing required spatial columns x or y".to_string(), + ) + .into()); + } + if !has_period { + return Err(TrackingDataContainerErrorKind::InvalidParameters( + "Missing required column period or period_id".to_string(), + ) + .into()); + } + let frame_idx_col = if let Some(fi) = frame_id_idx && columns[fi].null_count() < columns[fi].len() { @@ -445,7 +464,7 @@ impl TrackingDataContainer { } else { frame_id_idx.or(timestamp_idx).ok_or_else(|| { TrackingDataContainerErrorKind::InvalidParameters( - "Missing frame_id or timestamp column".to_string(), + "Missing required frame_id or timestamp column".to_string(), ) })? }; @@ -455,12 +474,12 @@ impl TrackingDataContainer { let player_id_idx = player_id_idx.ok_or_else(|| { TrackingDataContainerErrorKind::InvalidParameters( - "Missing player/id or player_id column".to_string(), + "Missing required entity_id or player_id column".to_string(), ) })?; let team_id_idx = team_id_idx.ok_or_else(|| { TrackingDataContainerErrorKind::InvalidParameters( - "Missing team/id or team_id column".to_string(), + "Missing team_id or team/id column".to_string(), ) })?; diff --git a/python/tests/test_advanced_features.py b/python/tests/test_advanced_features.py index 507ef4f..48c0b26 100644 --- a/python/tests/test_advanced_features.py +++ b/python/tests/test_advanced_features.py @@ -76,10 +76,12 @@ def test_pivot_global_constants(): "match/id": ["constant_match"] * 4, "competition/id": ["constant_comp"] * 4, "timestamp": [1000, 1000, 2000, 2000], + "period": ["1", "1", "1", "1"], "player/id": ["p1", "p2", "p1", "p2"], "team/id": ["home", "home", "home", "home"], "entity_key": ["p1|home", "p2|home", "p1|home", "p2|home"], "x": [1.0, 2.0, 3.0, 4.0], + "y": [1.0, 2.0, 3.0, 4.0], } df = pl.DataFrame(data) diff --git a/python/tests/test_catalog.py b/python/tests/test_catalog.py index 4deebea..b12ea1c 100644 --- a/python/tests/test_catalog.py +++ b/python/tests/test_catalog.py @@ -6,15 +6,16 @@ def test_catalog_lifecycle(tmp_path): catalog = tdc.TrackingDataCatalog(str(warehouse_path)) catalog.initialize() - # Create a container + # Create a container with all required fields for Iceberg data = { "match/id": ["match_1", "match_1"], "timestamp": [1000000, 1040000], # microseconds - "ball/player_id": ["7", "7"], - "ball/team_id": ["home", "home"], + "frame_id": [100, 101], + "period": ["1", "1"], "ball/x": [0.0, 1.0], "ball/y": [0.0, 1.0], } + # Note: player/id and team/id will be automatically added by melt() for the ball container = tdc.TrackingDataContainer(data, validate=False) # Save container @@ -31,8 +32,7 @@ def test_catalog_lifecycle(tmp_path): # Check data df = loaded.to_polars() assert df["match/id"][0] == "match_1" - # ball/player_id was an input, but after melt/pivot it becomes ball/player/id - # as player/id is a core attribute. + # player/id was an input, and stays as player/id in wide format assert "ball/player/id" in df.columns assert df["ball/x"][0] == 0.0 @@ -42,12 +42,12 @@ def test_bound_container_save(tmp_path): catalog = tdc.TrackingDataCatalog(str(warehouse_path)) catalog.initialize() - # Create and save + # Create and save with required fields data = { "match/id": ["match_1"], "timestamp": [1000000], - "ball/player_id": ["7"], - "ball/team_id": ["home"], + "frame_id": [100], + "period": ["1"], "ball/x": [0.0], "ball/y": [0.0], } diff --git a/python/tracking_data_container/catalog.py b/python/tracking_data_container/catalog.py index c0b07aa..fef5fd5 100644 --- a/python/tracking_data_container/catalog.py +++ b/python/tracking_data_container/catalog.py @@ -46,7 +46,7 @@ "match/id": "match_id", "competition/id": "competition_id", "season/id": "season_id", - "player/id": "player_id", + "player/id": "entity_id", "team/id": "team_id", } @@ -143,26 +143,26 @@ def _create_tracking_table(self) -> None: except NoSuchTableError: pass # Create new table - # Define the requested schema with snake_case names and required fields. + # Define the requested schema with specific order and requirements. schema = Schema( - NestedField(1, "match_id", StringType(), required=True), - NestedField(2, "competition_id", StringType(), required=False), - NestedField(3, "season_id", StringType(), required=False), - NestedField(4, "timestamp", TimestampType(), required=False), - NestedField(5, "frame_id", LongType(), required=False), - NestedField(6, "period", StringType(), required=False), + NestedField(1, "competition_id", StringType(), required=False), + NestedField(2, "season_id", StringType(), required=False), + NestedField(3, "match_id", StringType(), required=True), + NestedField(4, "frame_id", LongType(), required=True), + NestedField(5, "period", StringType(), required=True), + NestedField(6, "timestamp", TimestampType(), required=True), NestedField(7, "entity_type", StringType(), required=True), - NestedField(8, "player_id", StringType(), required=False), + NestedField(8, "entity_id", StringType(), required=True), NestedField(9, "team_id", StringType(), required=False), - NestedField(10, "x", FloatType(), required=False), - NestedField(11, "y", FloatType(), required=False), + NestedField(10, "x", FloatType(), required=True), + NestedField(11, "y", FloatType(), required=True), NestedField(12, "z", FloatType(), required=False), ) # Define partitioning by match_id partition_spec = PartitionSpec( PartitionField( - source_id=1, field_id=1000, transform=IdentityTransform(), name="match_id" + source_id=3, field_id=1000, transform=IdentityTransform(), name="match_id" ) ) @@ -184,7 +184,7 @@ def _create_metadata_table(self) -> None: except NoSuchTableError: pass - # Define schema for metadata as requested + # Define schema for metadata schema = Schema( NestedField(1, "match_id", StringType(), required=True), NestedField(2, "competition_id", StringType(), required=False), @@ -209,7 +209,6 @@ def load_container( Returns a LazyTrackingDataContainer that wraps the physical long-format Iceberg table but allows logical wide-format analysis. """ - # Optimized truly lazy loading: don't call to_arrow() yet! return CatalogLazyTrackingDataContainer( self, match_id=match_id, player_id=player_id, **filters ) @@ -281,11 +280,9 @@ def _save_tracking_data( .alias("entity_type") ) else: - # Fallback if entity_key is missing (shouldn't happen with container.melt()) raise ValueError("entity_key column is required for saving to catalog") # Handle potential name collisions before renaming - # If any target names already exist, drop them to_drop = [ v for k, v in COLUMN_MAPPING.items() if k in long_df.columns and v in long_df.columns ] @@ -299,16 +296,27 @@ def _save_tracking_data( # Ensure match_id is present and correct long_df = long_df.with_columns(pl.lit(match_id).alias("match_id")) - # Check required fields - if long_df["match_id"].null_count() > 0: - raise ValueError("match_id cannot be null") - if long_df["entity_type"].null_count() > 0: - raise ValueError("entity_type cannot be null") + # 3. Validation of required fields + required_fields = [ + "match_id", + "frame_id", + "period", + "timestamp", + "entity_type", + "entity_id", + "x", + "y", + ] + for field in required_fields: + if field not in long_df.columns: + raise ValueError(f"Required field {field} is missing from container") + if long_df[field].null_count() > 0: + raise ValueError(f"Required field {field} contains null values") - # 3. Convert to Arrow and handle dictionary casting + # 4. Convert to Arrow and handle types arrow_table = long_df.to_arrow() - # Define known types for core columns to avoid incorrect casting when null + # Define known types for Iceberg compatibility known_types = { "timestamp": pa.timestamp("us"), "frame_id": pa.int64(), @@ -317,9 +325,6 @@ def _save_tracking_data( "z": pa.float32(), } - # Cast dictionary columns to their value type (usually string), - # null types to string (or known type) for Iceberg compatibility, - # and ensure timestamp is correctly typed. new_columns = [] new_fields = [] for i, field in enumerate(arrow_table.schema): @@ -340,9 +345,9 @@ def _save_tracking_data( arrow_table = pa.Table.from_arrays(new_columns, schema=pa.schema(new_fields)) - # Start transaction for atomic delete + append + schema update + # Start transaction with self._tracking_table.transaction() as txn: - # Update schema to include any new metrics (e.g. speed, acc) + # Update schema to include any new metrics container_schema = _pyarrow_to_schema_without_ids(arrow_table.schema) txn.update_schema().union_by_name(container_schema).commit() @@ -406,7 +411,6 @@ def __init__( limit: int | None = None, **filters: Any, ): - # We pass None for the inner Rust container as we manage the scan in collect() super().__init__(None, catalog=catalog, is_long_storage=True) # type: ignore self._catalog: TrackingDataCatalog = catalog self._match_id = match_id @@ -423,7 +427,6 @@ def schema(self) -> Fields: assert self._catalog is not None - # Fast path: just get unique entities for this match if self._catalog._tracking_table is None: self._catalog._tracking_table = self._catalog._iceberg_catalog.load_table( "tracking.tracking_data" @@ -435,12 +438,12 @@ def schema(self) -> Fields: if self._player_id: filter_expr = And( filter_expr, - EqualTo(term=Reference("player_id"), value=self._player_id), # type: ignore + EqualTo(term=Reference("entity_id"), value=self._player_id), # type: ignore ) - # Only scan player_id and team_id to reconstruct schema + # Scan entity_id and team_id to reconstruct schema entities_table = self._catalog._tracking_table.scan( - row_filter=filter_expr, selected_fields=("player_id", "team_id") + row_filter=filter_expr, selected_fields=("entity_id", "team_id") ).to_arrow() entities_df = cast(pl.DataFrame, pl.from_arrow(entities_table)) @@ -448,7 +451,7 @@ def schema(self) -> Fields: entities_df = entities_df.to_frame() entities_df = entities_df.unique() - # All potential physical attributes (x, y, z, etc.) + # All potential physical attributes physical_names = [f.name for f in self._catalog._tracking_table.schema().fields] # Ignore core and metadata columns ignored = set(COLUMN_MAPPING.values()) | { @@ -459,11 +462,9 @@ def schema(self) -> Fields: "period_id", "entity_key", } - # Anything else is an attribute (like x, y, z, player_id, team_id, etc.) attr_cols = [c for c in physical_names if c not in ignored and c != "id"] - # Base list: all core columns that are NOT entity-specific (internal names) - # We use COLUMN_MAPPING to check if they exist physically + # Base list: all core columns that are NOT entity-specific wide_names = [] for c in CORE_COLUMNS: if c in ("player/id", "team/id", "entity_key"): @@ -473,14 +474,14 @@ def schema(self) -> Fields: wide_names.append(c) for row in entities_df.to_dicts(): - p_id = str(row.get("player_id")) + p_id = str(row.get("entity_id")) t_id = str(row.get("team_id", "none")) prefix = TrackedEntity.from_entity_key(f"{p_id}|{t_id}").cdf_prefix() for attr in attr_cols: wide_names.append(f"{prefix}{attr}") - # Also add player/id and team/id as attributes for this entity if they exist physically - if "player_id" in physical_names: + # Map back entity_id and team_id as attributes with standard CDF names + if "entity_id" in physical_names: wide_names.append(f"{prefix}player/id") if "team_id" in physical_names: wide_names.append(f"{prefix}team/id") @@ -527,7 +528,7 @@ def collect(self) -> TrackingDataContainer: if self._player_id: filter_expr = And( filter_expr, - EqualTo(term=Reference("player_id"), value=self._player_id), # type: ignore + EqualTo(term=Reference("entity_id"), value=self._player_id), # type: ignore ) # 1. Determine which long-format columns we need @@ -535,7 +536,6 @@ def collect(self) -> TrackingDataContainer: deferred_select = None if self._selection: - # Reconstruct which long columns are needed for this wide selection eager_schema = self.schema() literal_names: list[str] = [] input_items = ( @@ -577,17 +577,17 @@ def collect(self) -> TrackingDataContainer: for req in [ "match_id", "entity_type", - "player_id", + "entity_id", "team_id", "frame_id", "timestamp", + "period", + "x", + "y", ]: if req in physical_names: selected_long_fields_set.add(req) - # Include any other physical column that might be an attribute - # (If we selected an attribute like 'x', it was already added above via field.attribute) - selected_long_fields = list(selected_long_fields_set) # 2. Add entity filter pushdown @@ -596,7 +596,7 @@ def collect(self) -> TrackingDataContainer: for k in needed_ents: p_id, t_id = k.split("|") - cond: BooleanExpression = EqualTo(term=Reference("player_id"), value=p_id) # type: ignore + cond: BooleanExpression = EqualTo(term=Reference("entity_id"), value=p_id) # type: ignore if p_id != "ball" and t_id != "none": cond = And(cond, EqualTo(term=Reference("team_id"), value=t_id)) # type: ignore @@ -613,14 +613,12 @@ def collect(self) -> TrackingDataContainer: if selected_long_fields is not None: scan_kwargs["selected_fields"] = tuple(selected_long_fields) if self._limit: - # Rough estimate for long format: 50 rows per frame scan_kwargs["limit"] = self._limit * 50 scan = self._catalog._tracking_table.scan(**scan_kwargs) arrow_table = scan.to_arrow() # Rename back to CDF-like names for Rust pivot - # Handle potential collisions here too rename_map = {v: k for k, v in COLUMN_MAPPING.items() if v in arrow_table.column_names} if rename_map: new_fields = [] @@ -628,18 +626,12 @@ def collect(self) -> TrackingDataContainer: for field in arrow_table.schema: if field.name in rename_map: target_name = rename_map[field.name] - # If target name already exists, we prefer the one being renamed if target_name in arrow_table.column_names: - # Skip this column, we'll use the renamed one later continue new_fields.append(pa.field(target_name, field.type, field.nullable)) new_columns.append(arrow_table.column(field.name)) else: - # Keep original name if field.name in rename_map.values(): - # This name is a target of another rename, and we already handled it - # above (by skipping it if it was a source, but here it's an existing - # column with target name). We skip it here to avoid duplicates. continue new_fields.append(field) new_columns.append(arrow_table.column(field.name)) diff --git a/python/tracking_data_container/container.py b/python/tracking_data_container/container.py index d90cd97..d7b1732 100644 --- a/python/tracking_data_container/container.py +++ b/python/tracking_data_container/container.py @@ -317,9 +317,14 @@ def melt(self) -> pl.DataFrame: .alias("entity_key"), ] ) + # Ensure spatial columns are float32 for consistency + for spatial in ["x", "y", "z"]: + if spatial in t.columns: + t = t.with_columns(pl.col(spatial).cast(pl.Float32)) + t = t.filter(~pl.all_horizontal(pl.col(list(attrs.keys())).is_null())) dfs.append(t) - return pl.concat(dfs, how="diagonal") if dfs else pl.DataFrame() + return pl.concat(dfs, how="diagonal_relaxed") if dfs else pl.DataFrame() def __arrow_c_schema__(self) -> Any: return self._container.__arrow_c_schema__() From ff2091201c9e14cfbbcab856dd4c36970b6243bb Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Wed, 8 Apr 2026 14:14:38 +0200 Subject: [PATCH 07/21] perf: store unique entities in metadata for fast schema reconstruction --- profile_load.py | 68 ++++-------- profile_warehouse/catalog.db | Bin 0 -> 20480 bytes ...-582c-4563-a6f7-935a13162b9f.metadata.json | 1 + ...-1c1a-4098-903a-41eec5ee454e.metadata.json | 1 + ...67bf9b-f807-48db-aae8-3a65e2db3451-m0.avro | Bin 0 -> 4487 bytes ...-3967bf9b-f807-48db-aae8-3a65e2db3451.avro | Bin 0 -> 1819 bytes ...-a026-4806-acfa-74075182b917.metadata.json | 1 + ...-d534-4ba2-b39c-44bd8e3935da.metadata.json | 1 + ...f6aa22-f608-4e8d-af73-eee758db29a1-m0.avro | Bin 0 -> 5445 bytes ...-acf6aa22-f608-4e8d-af73-eee758db29a1.avro | Bin 0 -> 1829 bytes python/tracking_data_container/catalog.py | 100 ++++++++++++++---- 11 files changed, 101 insertions(+), 71 deletions(-) create mode 100644 profile_warehouse/catalog.db create mode 100644 profile_warehouse/tracking/match_metadata/metadata/00000-64842057-582c-4563-a6f7-935a13162b9f.metadata.json create mode 100644 profile_warehouse/tracking/match_metadata/metadata/00001-8e94e659-1c1a-4098-903a-41eec5ee454e.metadata.json create mode 100644 profile_warehouse/tracking/match_metadata/metadata/3967bf9b-f807-48db-aae8-3a65e2db3451-m0.avro create mode 100644 profile_warehouse/tracking/match_metadata/metadata/snap-9142401099081597578-0-3967bf9b-f807-48db-aae8-3a65e2db3451.avro create mode 100644 profile_warehouse/tracking/tracking_data/metadata/00000-ccf96e5c-a026-4806-acfa-74075182b917.metadata.json create mode 100644 profile_warehouse/tracking/tracking_data/metadata/00001-6e5882b4-d534-4ba2-b39c-44bd8e3935da.metadata.json create mode 100644 profile_warehouse/tracking/tracking_data/metadata/acf6aa22-f608-4e8d-af73-eee758db29a1-m0.avro create mode 100644 profile_warehouse/tracking/tracking_data/metadata/snap-2502839378513990955-0-acf6aa22-f608-4e8d-af73-eee758db29a1.avro diff --git a/profile_load.py b/profile_load.py index 77abbba..e3f4165 100644 --- a/profile_load.py +++ b/profile_load.py @@ -16,11 +16,15 @@ def generate_synthetic_data(n_frames=146000, n_entities=25): frames = np.arange(n_frames) timestamps = frames * 40000 # 25 fps - data = { - "match/id": ["m1"] * n_frames, - "frame_id": frames, - "timestamp": timestamps, - "period": ["1"] * n_frames, + # To make it easier and faster to generate, we'll create the long format directly + print("Creating long-format dataframe...") + + # Repeat core columns for each entity + long_data = { + "match/id": np.tile(["m1"], n_frames * n_entities), + "frame_id": np.tile(frames, n_entities), + "timestamp": np.tile(timestamps, n_entities), + "period": np.tile(["1"], n_frames * n_entities), } # Entities @@ -40,17 +44,6 @@ def generate_synthetic_data(n_frames=146000, n_entities=25): entity_keys = entity_keys[:n_entities] - # To make it easier and faster to generate, we'll create the long format directly - print("Creating long-format dataframe...") - - # Repeat core columns for each entity - long_data = { - "match/id": np.tile(["m1"], n_frames * n_entities), - "frame_id": np.tile(frames, n_entities), - "timestamp": np.tile(timestamps, n_entities), - "period": np.tile(["1"], n_frames * n_entities), - } - # Entity columns p_ids = [] t_ids = [] @@ -101,46 +94,21 @@ def profile_load(): print("\n--- Profiling catalog.load_container(match_id='m1').collect() ---") - # Internal breakdown of collect() - # We'll do it manually to get the breakdown - - # 1. Iceberg scan + to_arrow - from pyiceberg.expressions import EqualTo, Reference - t0 = time.perf_counter() - table = catalog._iceberg_catalog.load_table("tracking.tracking_data") - scan = table.scan(row_filter=EqualTo(term=Reference("match/id"), value="m1")) - arrow_table = scan.to_arrow() - t_scan = time.perf_counter() - t0 - - # 2. to_polars conversion + # 1. Total collect() call t0 = time.perf_counter() - # This is what happens in TrackingDataContainer.from_arrow(...).to_polars() - # Actually CatalogLazyTrackingDataContainer.collect() does: - # container = TrackingDataContainer.from_arrow(arrow_table, catalog=self._catalog) - # wide_container = TrackingDataContainer.pivot(container.to_polars()) - - # from_arrow is very fast (just wraps), the work is in to_polars() - df_long = pl.from_arrow(arrow_table) - t_to_polars = time.perf_counter() - t0 + lazy_container = catalog.load_container(match_id="m1") + wide_container = lazy_container.collect() + t_collect = time.perf_counter() - t0 - # 3. pivot() call total - t0 = time.perf_counter() - _ = TrackingDataContainer.pivot(arrow_table) - t_pivot_total = time.perf_counter() - t0 + print(f"Total collect() time: {t_collect:.4f}s") + print(f"Wide container shape: {wide_container.num_rows()} rows, {len(wide_container.schema())} columns") - # Reconstruction of schema (Lazy.schema()) + # 2. Lazy.schema() reconstruction t0 = time.perf_counter() - lazy_container = catalog.load_container(match_id="m1") + lazy_container = catalog.load_container(match_id="m1") # new one to avoid cache _ = lazy_container.schema() t_schema = time.perf_counter() - t0 - - print(f"Detailed Breakdown:") - print(f" - Iceberg scan + to_arrow: {t_scan:.4f}s") - print(f" - to_polars conversion: {t_to_polars:.4f}s") - print(f" - Lazy.schema() reconstruction: {t_schema:.4f}s") - print(f" - pivot() call total: {t_pivot_total:.4f}s") - - print(f"\nTotal collect() estimated: {t_scan + t_pivot_total:.4f}s") + print(f"Lazy.schema() (metadata-based): {t_schema:.4f}s") if __name__ == "__main__": profile_load() diff --git a/profile_warehouse/catalog.db b/profile_warehouse/catalog.db new file mode 100644 index 0000000000000000000000000000000000000000..929d843eea55277712871594052370348ae4d6dd GIT binary patch literal 20480 zcmeI&O-~a+7zgm(Qr;S%9yCM|vqlqW!qDAzw=a6oicK^kXax-+WHa4PVWo6&cZ+as zI2b-NQ`#=JlfO3#I6|JKR^|(ltqz zrN@dQNzyRA2kG5B0@MgLJM<}^YWHdmOY@(vkB5Iq!Leg$q7wcxc06&rS1`^)00Izz z00bZa0SG_<0uVT#K=skU@MJbCAId(@mxOJJyl^+oGT#y2F1Lia>pHu_^=;wxyB1u? zWiu;TWhL`uF{|{8RAxh?7Wa9{Y0RR$&EyuIWpcAI!-yzL%PY##>f+*@5*lsg&^zQ; zatG$NI5lH&iqYr={>b0Bc6RW3e7p25 zsNX8n4(uJXBYa+3cTxzJ=>{x<{4u9l%}0c{f?be+9#+y4+BG! z%aOm8r#{4M7o^c2pl52N_FRom6r{k|i8OXH@#%ce8Ad?>0uX=z1Rwwb2tWV=5V({A zRaYLFnx3hySJw-o$SWn^ce%A~mp7ZAW^HL#v`gYaG#Xv?gzH6VS)8Z617U6XUi7Kh z6Q$b5L9}CpX*p$|+hsaW;kEJRNA8L(S_Fz#WUEo1B-&gYMrpaOYAhj)R4SI&S;2_w zOwaQe%g56e)Af8ICF1G0QQ-5kxEf0^J*6fXw~Cx4bv0>d z^aavdvNKJ(6Yft8cAlPVszsei8$klVv^Cl!?n1U zpyCue^9)gO0^uVmd=&nADW6y*1px>^00Izz00bZa0SG_<0ucDG1?~;feu><7vm|hr zVrE9WVmkJ%#>r3)f0O8i0|F3$00bZa0SG_<0uX=z1Rwx`D=KhDrY(S*H@da}#J=tM yp6^yf?e~8<{9U3K4hTR10uX=z1Rwwb2tWV=5P$##uCl7la6vtZxWuetbDWzd?os>43S27=key}1@609O=(E!bJxNqjZcIdpB;odtX zg*1l5pb3c!l?rud`q~st3=2295feWGY&6D&I}r=DF&aNMgoWqc>CBsXEu}3lX`1)u z&HbNqf9K<#`%2+NeZ%9-8YU%tZ{rz-j|D{ORT!tC#g&{=bcky+rFZa*;@C>BmQy@j z5^t0+w}wr4mP@U1ydn5b$;r^61YQRfGMd8{eF9n1<-0{5!2^6u*5(OhsU<9)@ zF78Ygmk7r;=GZ&|8V`0qpHm_{9Rh%w0}jOF4uGK@k`6o|Dts9SB191#Zn7+^ua>P4gc#?ra+xnk#=h(>l@mP5bTJ7g~#R^wH_=^szfF?UR?LTgt85KfKuT`_|T`trOD|e{Jf1bkn)d %i3pbBXe)(Ph)z_|m zKl4@HE%l0erT*Ec-mAZ1AFrcFE*_yz^fX?+aC6|F7xg}U;QWP88bA8@X MUELqo`m1;UKUBG&q5uE@ literal 0 HcmV?d00001 diff --git a/profile_warehouse/tracking/match_metadata/metadata/snap-9142401099081597578-0-3967bf9b-f807-48db-aae8-3a65e2db3451.avro b/profile_warehouse/tracking/match_metadata/metadata/snap-9142401099081597578-0-3967bf9b-f807-48db-aae8-3a65e2db3451.avro new file mode 100644 index 0000000000000000000000000000000000000000..c13dddee74417b7ab28df8948d06b0b6a4104517 GIT binary patch literal 1819 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 literal 0 HcmV?d00001 diff --git a/profile_warehouse/tracking/tracking_data/metadata/snap-2502839378513990955-0-acf6aa22-f608-4e8d-af73-eee758db29a1.avro b/profile_warehouse/tracking/tracking_data/metadata/snap-2502839378513990955-0-acf6aa22-f608-4e8d-af73-eee758db29a1.avro new file mode 100644 index 0000000000000000000000000000000000000000..58e72bcd90494c295b60b21d25c787d928affe75 GIT binary patch literal 1829 zcmb7ETSyd97%sbLXx4)lOtWQ-B-*vvU8mjEhzbo9qtK)W%i-+I+1<&xHFHK?Weo$n zXuX6!ln>HHqKAZ_2MHpdimaDxh0mM;d*1b&x~p?yJr$R7wYUVkV=hZtt33N$Dq4-u9l8=_dQ!?OmIIp~oMDGaqN zZ&^gqB!E1fP}2oP&hiz2PED!e6drQ0? zT~tJXBCq4Owq8mGQu0> zwYkC7C7_t=NQ*0}52=LPwS(SBUP47wu*L#3HVFx)4y&t>V{v9yDOQ#~0x!sD zA)HfWgiT%y@&=qhRs+~n18iW10NXQ`1e|NSh08JQb7TNK4|&#_i=}GzuL2(tM5wcz zV%W>4s|mc#GuSQY2sfK`Vjw-RG4N1?2$$eQKw4lUNI9S>UCR{UC7W(kYl zkaawUNtM(Muoy$c5QG&)gg~}0PYRBIaB%X1q|sh8pY(CaR6$uZr|~B~Ou!HkPS;dl zu+*Z`1vNH|N7J^>UgGEwym16|Y6Vt9RhMNYj?5}pO(E+&tE|q&D^+zgc{F zQ~mXWOldB427E57Dhj-p)Ea6ALZ?NvX|Sjb);@_ G?*9PgNmTy; literal 0 HcmV?d00001 diff --git a/python/tracking_data_container/catalog.py b/python/tracking_data_container/catalog.py index fef5fd5..5496a16 100644 --- a/python/tracking_data_container/catalog.py +++ b/python/tracking_data_container/catalog.py @@ -237,11 +237,15 @@ def save_container( else: raise ValueError("match_id must be provided if not present in container data") - # Save tracking data - self._save_tracking_data(container, match_id) + # 1. Save tracking data and get unique entities + entities = self._save_tracking_data(container, match_id) - # Save metadata - self._save_metadata(metadata or {}, match_id) + # 2. Prepare metadata + final_metadata = metadata.copy() if metadata else {} + final_metadata["_entities"] = entities + + # 3. Save metadata + self._save_metadata(final_metadata, match_id) def commit_container(self, container: TrackingDataContainer) -> None: """ @@ -256,12 +260,43 @@ def commit_container(self, container: TrackingDataContainer) -> None: match_id = df["match/id"][0] if "match/id" in df.columns else df["match_id"][0] self.save_container(container, match_id=str(match_id)) + def get_metadata(self, match_id: str) -> dict: + """ + Load metadata for a match. + + Args: + match_id: ID of the match + + Returns: + Metadata dictionary + """ + if self._metadata_table is None: + self._metadata_table = self._iceberg_catalog.load_table("tracking.match_metadata") + + assert self._metadata_table is not None + + scan = self._metadata_table.scan( + row_filter=EqualTo(term=Reference("match_id"), value=match_id) # type: ignore + ) + arrow_table = scan.to_arrow() + + if len(arrow_table) == 0: + return {} + + metadata_json = arrow_table.column("metadata_json")[0].as_py() + return cast(dict[Any, Any], json.loads(metadata_json)) + def _save_tracking_data( self, container: TrackingDataContainer, match_id: str, - ) -> None: - """Save tracking data to Iceberg table in entity-long format.""" + ) -> list[dict[str, str]]: + """ + Save tracking data to Iceberg table in entity-long format. + + Returns: + List of unique entities (dict with entity_id, team_id) + """ if self._tracking_table is None: self._tracking_table = self._iceberg_catalog.load_table("tracking.tracking_data") @@ -296,6 +331,13 @@ def _save_tracking_data( # Ensure match_id is present and correct long_df = long_df.with_columns(pl.lit(match_id).alias("match_id")) + # Extract unique entities before saving + entities = ( + long_df.select(["entity_id", "team_id"]).unique().collect().to_dicts() + if isinstance(long_df, pl.LazyFrame) + else long_df.select(["entity_id", "team_id"]).unique().to_dicts() + ) + # 3. Validation of required fields required_fields = [ "match_id", @@ -357,6 +399,8 @@ def _save_tracking_data( # Append long-format data txn.append(arrow_table) + return entities + def _save_metadata(self, metadata: dict, match_id: str) -> None: """Save metadata to Iceberg table.""" if self._metadata_table is None: @@ -434,23 +478,37 @@ def schema(self) -> Fields: assert self._catalog._tracking_table is not None - filter_expr: BooleanExpression = EqualTo(term=Reference("match_id"), value=self._match_id) # type: ignore - if self._player_id: - filter_expr = And( - filter_expr, - EqualTo(term=Reference("entity_id"), value=self._player_id), # type: ignore - ) + # 1. Try to get entities from metadata (optimized path) + metadata = self._catalog.get_metadata(self._match_id) + entities = metadata.get("_entities") + + if entities is None: + logger.info("Metadata missing _entities, falling back to data scan") + filter_expr: BooleanExpression = EqualTo( + term=Reference("match_id"), value=cast(Any, self._match_id) + ) # type: ignore + if self._player_id: + filter_expr = And( + filter_expr, + EqualTo(term=Reference("entity_id"), value=cast(Any, self._player_id)), # type: ignore + ) - # Scan entity_id and team_id to reconstruct schema - entities_table = self._catalog._tracking_table.scan( - row_filter=filter_expr, selected_fields=("entity_id", "team_id") - ).to_arrow() + # Scan entity_id and team_id to reconstruct schema + entities_table = self._catalog._tracking_table.scan( + row_filter=filter_expr, selected_fields=("entity_id", "team_id") + ).to_arrow() - entities_df = cast(pl.DataFrame, pl.from_arrow(entities_table)) - if isinstance(entities_df, pl.Series): - entities_df = entities_df.to_frame() - entities_df = entities_df.unique() + entities_df = cast(pl.DataFrame, pl.from_arrow(entities_table)) + if isinstance(entities_df, pl.Series): + entities_df = entities_df.to_frame() + entities_df = entities_df.unique() + entities = entities_df.to_dicts() + + # 2. Apply player filter if requested + if self._player_id: + entities = [e for e in entities if str(e.get("entity_id")) == self._player_id] + # 3. Reconstruct wide field names # All potential physical attributes physical_names = [f.name for f in self._catalog._tracking_table.schema().fields] # Ignore core and metadata columns @@ -473,7 +531,7 @@ def schema(self) -> Fields: if phys_name in physical_names: wide_names.append(c) - for row in entities_df.to_dicts(): + for row in entities: p_id = str(row.get("entity_id")) t_id = str(row.get("team_id", "none")) From 4b9332af120ec12f27dd29638d1720ab0ead13d5 Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Wed, 8 Apr 2026 14:17:24 +0200 Subject: [PATCH 08/21] cleanup: remove backward compatibility and streamline catalog schema flow --- crates/tdc-core/src/datacontainer/core.rs | 82 ++++++++++++--------- python/tracking_data_container/catalog.py | 88 ++++++----------------- 2 files changed, 69 insertions(+), 101 deletions(-) diff --git a/crates/tdc-core/src/datacontainer/core.rs b/crates/tdc-core/src/datacontainer/core.rs index b8baebd..7e36022 100644 --- a/crates/tdc-core/src/datacontainer/core.rs +++ b/crates/tdc-core/src/datacontainer/core.rs @@ -385,20 +385,17 @@ impl TrackingDataContainer { let fields = data.fields(); let columns = data.columns(); - // 1. Identify key columns + // 1. Identify key columns (Strictly using snake_case names) let mut frame_id_idx = None; let mut timestamp_idx = None; - let mut player_id_idx = None; + let mut entity_id_idx = None; let mut team_id_idx = None; let mut attr_indices = Vec::new(); let mut core_indices = Vec::new(); let core_names: AHashSet<&str> = [ - "match/id", "match_id", - "competition/id", "competition_id", - "season/id", "season_id", "timestamp", "frame_id", @@ -419,16 +416,15 @@ impl TrackingDataContainer { } else if name == "timestamp" { timestamp_idx = Some(i); core_indices.push(i); - } else if name == "player/id" || name == "player_id" || name == "entity_id" { - player_id_idx = Some(i); - attr_indices.push(i); // Also pivot it - } else if name == "team/id" || name == "team_id" { + } else if name == "entity_id" { + entity_id_idx = Some(i); + attr_indices.push(i); + } else if name == "team_id" { team_id_idx = Some(i); - attr_indices.push(i); // Also pivot it + attr_indices.push(i); } else if core_names.contains(name) { core_indices.push(i); } else if name == "entity_type" { - // Skip entity_type metadata during pivot continue; } else { attr_indices.push(i); @@ -472,15 +468,13 @@ impl TrackingDataContainer { let frame_col = &columns[frame_idx_col]; let is_frame_id = fields[frame_idx_col].name() == "frame_id"; - let player_id_idx = player_id_idx.ok_or_else(|| { + let entity_id_idx = entity_id_idx.ok_or_else(|| { TrackingDataContainerErrorKind::InvalidParameters( - "Missing required entity_id or player_id column".to_string(), + "Missing required entity_id column".to_string(), ) })?; let team_id_idx = team_id_idx.ok_or_else(|| { - TrackingDataContainerErrorKind::InvalidParameters( - "Missing team_id or team/id column".to_string(), - ) + TrackingDataContainerErrorKind::InvalidParameters("Missing team_id column".to_string()) })?; // 3. Determine min/max frame index for fixed builder if possible @@ -514,7 +508,6 @@ impl TrackingDataContainer { } } - // Only use fixed builder if it's a frame_id and the range is reasonable let range = if min_f <= max_f { (max_f - min_f) as usize } else { @@ -536,8 +529,15 @@ impl TrackingDataContainer { .iter() .map(|&idx| { let col_name = fields[idx].name(); + // Map back to internal CDF-like name for consistency in wide format if needed + let wide_name = match col_name.as_str() { + "match_id" => "match/id", + "competition_id" => "competition/id", + "season_id" => "season/id", + _ => col_name.as_str(), + }; let dtype = fields[idx].data_type().clone(); - let builder_idx = builder.get_or_create_column_id(col_name, dtype.clone()); + let builder_idx = builder.get_or_create_column_id(wide_name, dtype.clone()); CoreColumn { idx, builder_idx, @@ -568,7 +568,6 @@ impl TrackingDataContainer { ridx }; - // Insert core columns (always check for NULLs, even if same frame) for c in &core_cols { let col = &columns[c.idx]; if !col.is_null(i) { @@ -613,24 +612,23 @@ impl TrackingDataContainer { } } - // Insert attribute columns - let player_col = &columns[player_id_idx]; - if player_col.is_null(i) { + let ent_id_col = &columns[entity_id_idx]; + if ent_id_col.is_null(i) { continue; } - let p_id = match player_col.data_type() { - Utf8 => player_col.as_string::().value(i), - LargeUtf8 => player_col.as_string::().value(i), + let p_id = match ent_id_col.data_type() { + Utf8 => ent_id_col.as_string::().value(i), + LargeUtf8 => ent_id_col.as_string::().value(i), _ => continue, }; - let team_col = &columns[team_id_idx]; - let t_id = if team_col.is_null(i) { + let t_id_col = &columns[team_id_idx]; + let t_id = if t_id_col.is_null(i) { "none" } else { - match team_col.data_type() { - Utf8 => team_col.as_string::().value(i), - LargeUtf8 => team_col.as_string::().value(i), + match t_id_col.data_type() { + Utf8 => t_id_col.as_string::().value(i), + LargeUtf8 => t_id_col.as_string::().value(i), _ => "none", } }; @@ -657,10 +655,26 @@ impl TrackingDataContainer { } }; - let attribute = crate::schema::TrackedEntityAttribute::from_str(attr_name) - .unwrap_or_else(|_| { - crate::schema::TrackedEntityAttribute::Other(attr_name.to_string()) - }); + let (attribute, _final_name) = match attr_name.as_str() { + "entity_id" => ( + crate::schema::TrackedEntityAttribute::from_str("player/id") + .unwrap(), + "player/id".to_string(), + ), + "team_id" => ( + crate::schema::TrackedEntityAttribute::from_str("team/id").unwrap(), + "team/id".to_string(), + ), + _ => ( + crate::schema::TrackedEntityAttribute::from_str(attr_name) + .unwrap_or_else(|_| { + crate::schema::TrackedEntityAttribute::Other( + attr_name.to_string(), + ) + }), + attr_name.to_string(), + ), + }; let field = DataField::TrackedEntity { entity, attribute }; builder.get_or_create_column_id( diff --git a/python/tracking_data_container/catalog.py b/python/tracking_data_container/catalog.py index 5496a16..033dea6 100644 --- a/python/tracking_data_container/catalog.py +++ b/python/tracking_data_container/catalog.py @@ -276,7 +276,7 @@ def get_metadata(self, match_id: str) -> dict: assert self._metadata_table is not None scan = self._metadata_table.scan( - row_filter=EqualTo(term=Reference("match_id"), value=match_id) # type: ignore + row_filter=EqualTo(term=Reference("match_id"), value=cast(Any, match_id)) # type: ignore ) arrow_table = scan.to_arrow() @@ -317,13 +317,6 @@ def _save_tracking_data( else: raise ValueError("entity_key column is required for saving to catalog") - # Handle potential name collisions before renaming - to_drop = [ - v for k, v in COLUMN_MAPPING.items() if k in long_df.columns and v in long_df.columns - ] - if to_drop: - long_df = long_df.drop(to_drop) - # Rename columns to Iceberg format rename_dict = {k: v for k, v in COLUMN_MAPPING.items() if k in long_df.columns} long_df = long_df.rename(rename_dict) @@ -332,11 +325,7 @@ def _save_tracking_data( long_df = long_df.with_columns(pl.lit(match_id).alias("match_id")) # Extract unique entities before saving - entities = ( - long_df.select(["entity_id", "team_id"]).unique().collect().to_dicts() - if isinstance(long_df, pl.LazyFrame) - else long_df.select(["entity_id", "team_id"]).unique().to_dicts() - ) + entities = long_df.select(["entity_id", "team_id"]).unique().to_dicts() # 3. Validation of required fields required_fields = [ @@ -394,7 +383,7 @@ def _save_tracking_data( txn.update_schema().union_by_name(container_schema).commit() # Delete existing match partition - txn.delete(EqualTo(term=Reference("match_id"), value=match_id)) # type: ignore + txn.delete(EqualTo(term=Reference("match_id"), value=cast(Any, match_id))) # type: ignore # Append long-format data txn.append(arrow_table) @@ -431,7 +420,7 @@ def _save_metadata(self, metadata: dict, match_id: str) -> None: # Upsert metadata with self._metadata_table.transaction() as txn: - txn.delete(EqualTo(term=Reference("match_id"), value=match_id)) # type: ignore + txn.delete(EqualTo(term=Reference("match_id"), value=cast(Any, match_id))) # type: ignore txn.append(metadata_row) def __repr__(self) -> str: @@ -465,7 +454,7 @@ def __init__( self._cached_schema: Fields | None = None def schema(self) -> Fields: - """Reconstruct the wide schema by inspecting entities in the match.""" + """Reconstruct the wide schema by inspecting entities in the match metadata.""" if self._cached_schema: return self._cached_schema @@ -478,40 +467,19 @@ def schema(self) -> Fields: assert self._catalog._tracking_table is not None - # 1. Try to get entities from metadata (optimized path) + # Load entities from metadata (Strictly no fallback scan) metadata = self._catalog.get_metadata(self._match_id) entities = metadata.get("_entities") if entities is None: - logger.info("Metadata missing _entities, falling back to data scan") - filter_expr: BooleanExpression = EqualTo( - term=Reference("match_id"), value=cast(Any, self._match_id) - ) # type: ignore - if self._player_id: - filter_expr = And( - filter_expr, - EqualTo(term=Reference("entity_id"), value=cast(Any, self._player_id)), # type: ignore - ) + raise ValueError(f"Metadata for match {self._match_id} is missing unique entity list.") - # Scan entity_id and team_id to reconstruct schema - entities_table = self._catalog._tracking_table.scan( - row_filter=filter_expr, selected_fields=("entity_id", "team_id") - ).to_arrow() - - entities_df = cast(pl.DataFrame, pl.from_arrow(entities_table)) - if isinstance(entities_df, pl.Series): - entities_df = entities_df.to_frame() - entities_df = entities_df.unique() - entities = entities_df.to_dicts() - - # 2. Apply player filter if requested + # Apply player filter if requested if self._player_id: entities = [e for e in entities if str(e.get("entity_id")) == self._player_id] - # 3. Reconstruct wide field names - # All potential physical attributes + # Reconstruct wide field names physical_names = [f.name for f in self._catalog._tracking_table.schema().fields] - # Ignore core and metadata columns ignored = set(COLUMN_MAPPING.values()) | { "entity_type", "timestamp", @@ -520,6 +488,7 @@ def schema(self) -> Fields: "period_id", "entity_key", } + # Anything else is an attribute attr_cols = [c for c in physical_names if c not in ignored and c != "id"] # Base list: all core columns that are NOT entity-specific @@ -539,10 +508,8 @@ def schema(self) -> Fields: for attr in attr_cols: wide_names.append(f"{prefix}{attr}") # Map back entity_id and team_id as attributes with standard CDF names - if "entity_id" in physical_names: - wide_names.append(f"{prefix}player/id") - if "team_id" in physical_names: - wide_names.append(f"{prefix}team/id") + wide_names.append(f"{prefix}player/id") + wide_names.append(f"{prefix}team/id") self._cached_schema = Fields(list(dict.fromkeys(wide_names))) return self._cached_schema @@ -582,11 +549,13 @@ def collect(self) -> TrackingDataContainer: assert self._catalog._tracking_table is not None - filter_expr: BooleanExpression = EqualTo(term=Reference("match_id"), value=self._match_id) # type: ignore + filter_expr: BooleanExpression = EqualTo( + term=Reference("match_id"), value=cast(Any, self._match_id) + ) # type: ignore if self._player_id: filter_expr = And( filter_expr, - EqualTo(term=Reference("entity_id"), value=self._player_id), # type: ignore + EqualTo(term=Reference("entity_id"), value=cast(Any, self._player_id)), # type: ignore ) # 1. Determine which long-format columns we need @@ -654,9 +623,11 @@ def collect(self) -> TrackingDataContainer: for k in needed_ents: p_id, t_id = k.split("|") - cond: BooleanExpression = EqualTo(term=Reference("entity_id"), value=p_id) # type: ignore + cond: BooleanExpression = EqualTo( + term=Reference("entity_id"), value=cast(Any, p_id) + ) # type: ignore if p_id != "ball" and t_id != "none": - cond = And(cond, EqualTo(term=Reference("team_id"), value=t_id)) # type: ignore + cond = And(cond, EqualTo(term=Reference("team_id"), value=cast(Any, t_id))) # type: ignore if entity_predicate is None: entity_predicate = cond @@ -676,24 +647,7 @@ def collect(self) -> TrackingDataContainer: scan = self._catalog._tracking_table.scan(**scan_kwargs) arrow_table = scan.to_arrow() - # Rename back to CDF-like names for Rust pivot - rename_map = {v: k for k, v in COLUMN_MAPPING.items() if v in arrow_table.column_names} - if rename_map: - new_fields = [] - new_columns = [] - for field in arrow_table.schema: - if field.name in rename_map: - target_name = rename_map[field.name] - if target_name in arrow_table.column_names: - continue - new_fields.append(pa.field(target_name, field.type, field.nullable)) - new_columns.append(arrow_table.column(field.name)) - else: - if field.name in rename_map.values(): - continue - new_fields.append(field) - new_columns.append(arrow_table.column(field.name)) - arrow_table = pa.Table.from_arrays(new_columns, schema=pa.schema(new_fields)) + # No more redundant renaming here! Rust pivot handles snake_case now. # 4. Pivot to wide format using high-performance Rust implementation wide_container = TrackingDataContainer.pivot(arrow_table) From 3190883071c3bfe3614658f7d9a9864b5c7ce7ed Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Wed, 8 Apr 2026 14:49:02 +0200 Subject: [PATCH 09/21] fix: resolve VariantNotFound panic in pivot and align tests with snake_case schema --- crates/tdc-core/src/datacontainer/core.rs | 42 +++---- examples/08-idsse-warehouse.ipynb | 110 +++++------------- examples/idsse_warehouse/catalog.db | Bin 20480 -> 20480 bytes ...4114-42f7-b5fa-7d54395c20ab.metadata.json} | 2 +- ...-9c65-44bb-baca-296106ac627b.metadata.json | 1 - ...-2206-4077-8bb8-cbf6c6e2b53a.metadata.json | 1 + ...-b0b4-42c7-a2dc-f6178fd8f4ef.metadata.json | 1 - ...-e43b-45a3-845c-c66466ac7d83.metadata.json | 1 + ...-1f7c-4788-b217-5191127f4512.metadata.json | 1 - ...-6b42-4e78-89c0-4d9ff6e402d8.metadata.json | 1 + ...bf63c-5ea6-4c6d-b543-c7012c9be4c7-m0.avro} | Bin 4488 -> 4508 bytes ...24640-981e-48a3-b912-da48df7f23e7-m0.avro} | Bin 4489 -> 4509 bytes ...1b09a-d22e-4e7e-9606-c1ea5899be89-m0.avro} | Bin 4491 -> 4510 bytes ...63524640-981e-48a3-b912-da48df7f23e7.avro} | Bin 1821 -> 1829 bytes ...e501b09a-d22e-4e7e-9606-c1ea5899be89.avro} | Bin 1944 -> 1948 bytes ...540bf63c-5ea6-4c6d-b543-c7012c9be4c7.avro} | Bin 1893 -> 1901 bytes ...-51f9-42ff-b5ca-2617cf51350c.metadata.json | 1 + ...-3b69-41fb-a134-02dcfe960e12.metadata.json | 1 - ...-d7b2-434e-b5e2-6d9e732d9494.metadata.json | 1 + ...-6cd4-4964-a16c-803a68332e07.metadata.json | 1 - ...-d101-490a-9d46-11af2d07f6ad.metadata.json | 1 + ...-7b87-44a6-acc5-96b3d3d6fd5d.metadata.json | 1 - ...-5940-44b4-8fcd-39d9dc910155.metadata.json | 1 - ...-dc86-42b0-a991-fe9d25a19e5d.metadata.json | 1 + ...06ab25-5c95-4703-b985-e2a8252009d8-m0.avro | Bin 0 -> 6414 bytes ...34766b-2d66-44ac-b491-723a4772a1a3-m0.avro | Bin 6357 -> 0 bytes ...479d52-bc34-4f1d-a57d-7f1624b94057-m0.avro | Bin 0 -> 6389 bytes ...aa5e91-e6e1-4926-9601-15797fb2ce84-m0.avro | Bin 6351 -> 0 bytes ...947273-2bcb-44c7-8273-f5084c3de7bb-m0.avro | Bin 0 -> 6393 bytes ...582c4c-10e1-429d-8391-918bcb86bb3f-m0.avro | Bin 6335 -> 0 bytes ...-3faa5e91-e6e1-4926-9601-15797fb2ce84.avro | Bin 1852 -> 0 bytes ...1c479d52-bc34-4f1d-a57d-7f1624b94057.avro} | Bin 1936 -> 2001 bytes ...-78582c4c-10e1-429d-8391-918bcb86bb3f.avro | Bin 1999 -> 0 bytes ...-49947273-2bcb-44c7-8273-f5084c3de7bb.avro | Bin 0 -> 1849 bytes ...-0406ab25-5c95-4703-b985-e2a8252009d8.avro | Bin 0 -> 1938 bytes python/tests/test_advanced_features.py | 12 +- python/tracking_data_container/catalog.py | 4 +- 37 files changed, 66 insertions(+), 118 deletions(-) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{00000-9ab50098-b82d-4b0c-9ae9-1f40d1c3d73a.metadata.json => 00000-97b244bf-4114-42f7-b5fa-7d54395c20ab.metadata.json} (64%) delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00001-638b3294-9c65-44bb-baca-296106ac627b.metadata.json create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00001-b862312a-2206-4077-8bb8-cbf6c6e2b53a.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00002-e8eb68f3-b0b4-42c7-a2dc-f6178fd8f4ef.metadata.json create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00002-f0a06ef5-e43b-45a3-845c-c66466ac7d83.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00003-03d4420e-1f7c-4788-b217-5191127f4512.metadata.json create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00003-4a7176f5-6b42-4e78-89c0-4d9ff6e402d8.metadata.json rename examples/idsse_warehouse/tracking/match_metadata/metadata/{a858ff54-e99b-4dae-b66a-ac94a48da697-m0.avro => 540bf63c-5ea6-4c6d-b543-c7012c9be4c7-m0.avro} (88%) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{cb26b00d-1cfa-47c8-b859-bc2936a263b0-m0.avro => 63524640-981e-48a3-b912-da48df7f23e7-m0.avro} (88%) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{0bd0cfba-0ce6-4679-b7bb-4602f5f05406-m0.avro => e501b09a-d22e-4e7e-9606-c1ea5899be89-m0.avro} (89%) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{snap-3432642025755698322-0-cb26b00d-1cfa-47c8-b859-bc2936a263b0.avro => snap-4608038256352783609-0-63524640-981e-48a3-b912-da48df7f23e7.avro} (83%) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{snap-6599460958304115199-0-0bd0cfba-0ce6-4679-b7bb-4602f5f05406.avro => snap-5888034176243899453-0-e501b09a-d22e-4e7e-9606-c1ea5899be89.avro} (75%) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{snap-3611677702575916172-0-a858ff54-e99b-4dae-b66a-ac94a48da697.avro => snap-7979098971138712334-0-540bf63c-5ea6-4c6d-b543-c7012c9be4c7.avro} (77%) create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00000-48856661-51f9-42ff-b5ca-2617cf51350c.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00000-7a3c0cd8-3b69-41fb-a134-02dcfe960e12.metadata.json create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00001-08c539b1-d7b2-434e-b5e2-6d9e732d9494.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00001-a4870a2b-6cd4-4964-a16c-803a68332e07.metadata.json create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00002-8be271f0-d101-490a-9d46-11af2d07f6ad.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00002-8e9395a3-7b87-44a6-acc5-96b3d3d6fd5d.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00003-34c7dd19-5940-44b4-8fcd-39d9dc910155.metadata.json create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00003-e168fe8f-dc86-42b0-a991-fe9d25a19e5d.metadata.json create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/0406ab25-5c95-4703-b985-e2a8252009d8-m0.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/0e34766b-2d66-44ac-b491-723a4772a1a3-m0.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/1c479d52-bc34-4f1d-a57d-7f1624b94057-m0.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/3faa5e91-e6e1-4926-9601-15797fb2ce84-m0.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/49947273-2bcb-44c7-8273-f5084c3de7bb-m0.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/78582c4c-10e1-429d-8391-918bcb86bb3f-m0.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4345927569820710336-0-3faa5e91-e6e1-4926-9601-15797fb2ce84.avro rename examples/idsse_warehouse/tracking/tracking_data/metadata/{snap-4114664439361220711-0-0e34766b-2d66-44ac-b491-723a4772a1a3.avro => snap-4709260392851115659-0-1c479d52-bc34-4f1d-a57d-7f1624b94057.avro} (73%) delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-5344841726861284858-0-78582c4c-10e1-429d-8391-918bcb86bb3f.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-570726978145143711-0-49947273-2bcb-44c7-8273-f5084c3de7bb.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-7148257198741218111-0-0406ab25-5c95-4703-b985-e2a8252009d8.avro diff --git a/crates/tdc-core/src/datacontainer/core.rs b/crates/tdc-core/src/datacontainer/core.rs index 7e36022..7ba684b 100644 --- a/crates/tdc-core/src/datacontainer/core.rs +++ b/crates/tdc-core/src/datacontainer/core.rs @@ -416,12 +416,12 @@ impl TrackingDataContainer { } else if name == "timestamp" { timestamp_idx = Some(i); core_indices.push(i); - } else if name == "entity_id" { + } else if name == "entity_id" || name == "player/id" || name == "player_id" { entity_id_idx = Some(i); - attr_indices.push(i); - } else if name == "team_id" { + attr_indices.push(i); // Also pivot it + } else if name == "team_id" || name == "team/id" { team_id_idx = Some(i); - attr_indices.push(i); + attr_indices.push(i); // Also pivot it } else if core_names.contains(name) { core_indices.push(i); } else if name == "entity_type" { @@ -470,11 +470,13 @@ impl TrackingDataContainer { let entity_id_idx = entity_id_idx.ok_or_else(|| { TrackingDataContainerErrorKind::InvalidParameters( - "Missing required entity_id column".to_string(), + "Missing required entity_id or player/id column".to_string(), ) })?; let team_id_idx = team_id_idx.ok_or_else(|| { - TrackingDataContainerErrorKind::InvalidParameters("Missing team_id column".to_string()) + TrackingDataContainerErrorKind::InvalidParameters( + "Missing team_id or team/id column".to_string(), + ) })?; // 3. Determine min/max frame index for fixed builder if possible @@ -655,25 +657,19 @@ impl TrackingDataContainer { } }; - let (attribute, _final_name) = match attr_name.as_str() { - "entity_id" => ( - crate::schema::TrackedEntityAttribute::from_str("player/id") - .unwrap(), + let attribute = match attr_name.as_str() { + "entity_id" => crate::schema::TrackedEntityAttribute::Other( "player/id".to_string(), ), - "team_id" => ( - crate::schema::TrackedEntityAttribute::from_str("team/id").unwrap(), - "team/id".to_string(), - ), - _ => ( - crate::schema::TrackedEntityAttribute::from_str(attr_name) - .unwrap_or_else(|_| { - crate::schema::TrackedEntityAttribute::Other( - attr_name.to_string(), - ) - }), - attr_name.to_string(), - ), + "team_id" => { + crate::schema::TrackedEntityAttribute::Other("team/id".to_string()) + } + _ => crate::schema::TrackedEntityAttribute::from_str(attr_name) + .unwrap_or_else(|_| { + crate::schema::TrackedEntityAttribute::Other( + attr_name.to_string(), + ) + }), }; let field = DataField::TrackedEntity { entity, attribute }; diff --git a/examples/08-idsse-warehouse.ipynb b/examples/08-idsse-warehouse.ipynb index 044506e..dce2cfc 100644 --- a/examples/08-idsse-warehouse.ipynb +++ b/examples/08-idsse-warehouse.ipynb @@ -138,15 +138,15 @@ "text": [ "\n", "Processing match DFL-MAT-J03WMX...\n", - " ✅ Parsed in 1.52s, Saved in 1.92s\n", + " ✅ Parsed in 1.46s, Saved in 4.83s\n", " ✅ Rows: 145,967\n", "\n", "Processing match DFL-MAT-J03WN1...\n", - " ✅ Parsed in 1.27s, Saved in 1.54s\n", + " ✅ Parsed in 1.34s, Saved in 4.39s\n", " ✅ Rows: 141,561\n", "\n", "Processing match DFL-MAT-J03WOH...\n", - " ✅ Parsed in 1.18s, Saved in 1.43s\n", + " ✅ Parsed in 1.29s, Saved in 3.86s\n", " ✅ Rows: 137,214\n" ] } @@ -197,12 +197,30 @@ "name": "stdout", "output_type": "stream", "text": [ - "Warehouse Summary:\n", - " - Match DFL-MAT-J03WMX: 165,259 rows\n", - " - Match DFL-MAT-J03WN1: 162,273 rows\n", - " - Match DFL-MAT-J03WOH: 157,790 rows\n", + "Warehouse Summary:\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ "\n", - "Total rows in warehouse: 485,322\n" + "thread '' (21731647) panicked at crates/tdc-core/src/datacontainer/core.rs:661:38:\n", + "called `Result::unwrap()` on an `Err` value: VariantNotFound\n", + "note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace\n" + ] + }, + { + "ename": "PanicException", + "evalue": "called `Result::unwrap()` on an `Err` value: VariantNotFound", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mPanicException\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[5]\u001b[39m\u001b[32m, line 9\u001b[39m\n\u001b[32m 6\u001b[39m lazy_container = catalog.load_container(match_id=m_id)\n\u001b[32m 8\u001b[39m \u001b[38;5;66;03m# Execute plan and pivot to wide format\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m9\u001b[39m loaded = \u001b[43mlazy_container\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcollect\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 11\u001b[39m rows = loaded.num_rows()\n\u001b[32m 12\u001b[39m total_rows += rows\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/python/tracking_data_container/catalog.py:653\u001b[39m, in \u001b[36mCatalogLazyTrackingDataContainer.collect\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 648\u001b[39m arrow_table = scan.to_arrow()\n\u001b[32m 650\u001b[39m \u001b[38;5;66;03m# No more redundant renaming here! Rust pivot handles snake_case now.\u001b[39;00m\n\u001b[32m 651\u001b[39m \n\u001b[32m 652\u001b[39m \u001b[38;5;66;03m# 4. Pivot to wide format using high-performance Rust implementation\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m653\u001b[39m wide_container = \u001b[43mTrackingDataContainer\u001b[49m\u001b[43m.\u001b[49m\u001b[43mpivot\u001b[49m\u001b[43m(\u001b[49m\u001b[43marrow_table\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 654\u001b[39m wide_container._catalog = \u001b[38;5;28mself\u001b[39m._catalog\n\u001b[32m 656\u001b[39m \u001b[38;5;66;03m# Apply final selection/slice\u001b[39;00m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/python/tracking_data_container/container.py:376\u001b[39m, in \u001b[36mTrackingDataContainer.pivot\u001b[39m\u001b[34m(cls, df)\u001b[39m\n\u001b[32m 373\u001b[39m df = pa.RecordBatch.from_arrays(df.flatten(), names=[f.name \u001b[38;5;28;01mfor\u001b[39;00m f \u001b[38;5;129;01min\u001b[39;00m df.type])\n\u001b[32m 375\u001b[39m \u001b[38;5;66;03m# Use the underlying Rust implementation for performance\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m376\u001b[39m inner = \u001b[43m_TrackingDataContainer\u001b[49m\u001b[43m.\u001b[49m\u001b[43mpivot\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdf\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 377\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m TrackingDataContainer(inner, validate=\u001b[38;5;28;01mFalse\u001b[39;00m)\n", + "\u001b[31mPanicException\u001b[39m: called `Result::unwrap()` on an `Err` value: VariantNotFound" ] } ], @@ -234,62 +252,9 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Loading data for DFL-MAT-J03WMX...\n", - " ✅ Loaded and pivoted 165259 rows in 1.1780s\n" - ] - }, - { - "data": { - "text/html": [ - "

\n", - "shape: (5, 348)
match/idcompetition/idseason/idtimestampframe_idperiodteams/away/players/DFL-OBJ-0027G6/xteams/away/players/DFL-OBJ-0027G6/yteams/away/players/DFL-OBJ-0027G6/zteams/away/players/DFL-OBJ-0027G6/accteams/away/players/DFL-OBJ-0027G6/velteams/away/players/DFL-OBJ-0027G6/entity_keyteams/away/players/DFL-OBJ-0027G6/idteams/away/players/DFL-OBJ-0027G6/jersey_colourteams/away/players/DFL-OBJ-0027G6/namereferees/DFL-OBJ-000141/xreferees/DFL-OBJ-000141/yreferees/DFL-OBJ-000141/zreferees/DFL-OBJ-000141/accreferees/DFL-OBJ-000141/velreferees/DFL-OBJ-000141/entity_keyreferees/DFL-OBJ-000141/idreferees/DFL-OBJ-000141/jersey_colourreferees/DFL-OBJ-000141/nameteams/home/players/DFL-OBJ-0027B6/xteams/home/players/DFL-OBJ-0027B6/yteams/home/players/DFL-OBJ-0027B6/zteams/home/players/DFL-OBJ-0027B6/accteams/home/players/DFL-OBJ-0027B6/velteams/home/players/DFL-OBJ-0027B6/entity_keyteams/home/players/DFL-OBJ-0027B6/idteams/home/players/DFL-OBJ-0027B6/jersey_colourteams/home/players/DFL-OBJ-0027B6/nameteams/away/players/DFL-OBJ-0000RP/xteams/away/players/DFL-OBJ-0000RP/yteams/away/players/DFL-OBJ-0000RP/zteams/away/players/DFL-OBJ-0000RP/accteams/away/players/DFL-OBJ-0002DR/nameteams/away/players/DFL-OBJ-0027KL/xteams/away/players/DFL-OBJ-0027KL/yteams/away/players/DFL-OBJ-0027KL/zteams/away/players/DFL-OBJ-0027KL/accteams/away/players/DFL-OBJ-0027KL/velteams/away/players/DFL-OBJ-0027KL/entity_keyteams/away/players/DFL-OBJ-0027KL/idteams/away/players/DFL-OBJ-0027KL/jersey_colourteams/away/players/DFL-OBJ-0027KL/nameteams/home/players/DFL-OBJ-0002BO/xteams/home/players/DFL-OBJ-0002BO/yteams/home/players/DFL-OBJ-0002BO/zteams/home/players/DFL-OBJ-0002BO/accteams/home/players/DFL-OBJ-0002BO/velteams/home/players/DFL-OBJ-0002BO/entity_keyteams/home/players/DFL-OBJ-0002BO/idteams/home/players/DFL-OBJ-0002BO/jersey_colourteams/home/players/DFL-OBJ-0002BO/nameteams/home/players/DFL-OBJ-00012X/xteams/home/players/DFL-OBJ-00012X/yteams/home/players/DFL-OBJ-00012X/zteams/home/players/DFL-OBJ-00012X/accteams/home/players/DFL-OBJ-00012X/velteams/home/players/DFL-OBJ-00012X/entity_keyteams/home/players/DFL-OBJ-00012X/idteams/home/players/DFL-OBJ-00012X/jersey_colourteams/home/players/DFL-OBJ-00012X/nameteams/home/players/DFL-OBJ-0027V2/xteams/home/players/DFL-OBJ-0027V2/yteams/home/players/DFL-OBJ-0027V2/zteams/home/players/DFL-OBJ-0027V2/accteams/home/players/DFL-OBJ-0027V2/velteams/home/players/DFL-OBJ-0027V2/entity_keyteams/home/players/DFL-OBJ-0027V2/idteams/home/players/DFL-OBJ-0027V2/jersey_colourteams/home/players/DFL-OBJ-0027V2/name
strstrstrdatetime[μs]i64strf32f32f32f64f64strstrstrstrf32f32f32f64f64strstrstrstrf32f32f32f64f64strstrstrstrf32f32f32f64strf32f32f32f64f64strstrstrstrf32f32f32f64f64strstrstrstrf32f32f32f64f64strstrstrstrf32f32f32f64f64strstrstrstr
"DFL-MAT-J03WMX"nullnull2023-05-27 13:30:12.60010000"first_half"0.78-0.54null0.00.0"DFL-OBJ-0027G6|away"nullnullnull1.218.15null0.00.0"referee:DFL-OBJ-000141|none"nullnullnull20.167.08null0.00.0"DFL-OBJ-0027B6|home"nullnullnullnullnullnullnullnull-16.0499991.04null0.00.0"DFL-OBJ-0027KL|away"nullnullnull17.2620.85null0.00.0"DFL-OBJ-0002BO|home"nullnullnull13.08-13.71null0.00.0"DFL-OBJ-00012X|home"nullnullnullnullnullnullnullnullnullnullnullnull
"DFL-MAT-J03WMX"nullnull2023-05-27 13:30:12.64010001"first_half"0.85-0.56null2.347.74"DFL-OBJ-0027G6|away"nullnullnull1.2818.129999null0.676.96"referee:DFL-OBJ-000141|none"nullnullnull20.1200017.09null0.742.98"DFL-OBJ-0027B6|home"nullnullnullnullnullnullnullnull-15.980.99null0.667.27"DFL-OBJ-0027KL|away"nullnullnull17.2320.879999null1.084.43"DFL-OBJ-0002BO|home"nullnullnull13.06-13.75null1.43.49"DFL-OBJ-00012X|home"nullnullnullnullnullnullnullnullnullnullnullnull
"DFL-MAT-J03WMX"nullnull2023-05-27 13:30:12.68010002"first_half"0.94-0.59null2.288.08"DFL-OBJ-0027G6|away"nullnullnull1.3418.09null0.617.05"referee:DFL-OBJ-000141|none"nullnullnull20.097.1null0.653.09"DFL-OBJ-0027B6|home"nullnullnullnullnullnullnullnull-15.920.94null-0.047.32"DFL-OBJ-0027KL|away"nullnullnull17.1820.91null1.074.6"DFL-OBJ-0002BO|home"nullnullnull13.05-13.78null1.343.73"DFL-OBJ-00012X|home"nullnullnullnullnullnullnullnullnullnullnullnull
"DFL-MAT-J03WMX"nullnull2023-05-27 13:30:12.72010003"first_half"1.03-0.62null2.268.4"DFL-OBJ-0027G6|away"nullnullnull1.4118.059999null0.557.14"referee:DFL-OBJ-000141|none"nullnullnull20.0599997.12null0.563.18"DFL-OBJ-0027B6|home"nullnullnullnullnullnullnullnull-15.860.89null-0.747.26"DFL-OBJ-0027KL|away"nullnullnull17.1520.940001null1.034.74"DFL-OBJ-0002BO|home"nullnullnull13.03-13.82null1.243.95"DFL-OBJ-00012X|home"nullnullnullnullnullnullnullnullnullnullnullnull
"DFL-MAT-J03WMX"nullnull2023-05-27 13:30:12.76010004"first_half"1.12-0.65null2.298.74"DFL-OBJ-0027G6|away"nullnullnull1.4918.02null0.477.2"referee:DFL-OBJ-000141|none"nullnullnull20.027.13null0.443.25"DFL-OBJ-0027B6|home"nullnullnullnullnullnullnullnull-15.80.84null-1.47.1"DFL-OBJ-0027KL|away"nullnullnull17.11000120.98null0.984.88"DFL-OBJ-0002BO|home"nullnullnull13.02-13.87null1.114.13"DFL-OBJ-00012X|home"nullnullnullnullnullnullnullnullnullnullnullnull
" - ], - "text/plain": [ - "shape: (5, 348)\n", - "┌───────────┬───────────┬───────────┬───────────┬───┬───────────┬───────────┬───────────┬──────────┐\n", - "│ match/id ┆ competiti ┆ season/id ┆ timestamp ┆ … ┆ teams/hom ┆ teams/hom ┆ teams/hom ┆ teams/ho │\n", - "│ --- ┆ on/id ┆ --- ┆ --- ┆ ┆ e/players ┆ e/players ┆ e/players ┆ me/playe │\n", - "│ str ┆ --- ┆ str ┆ datetime[ ┆ ┆ /DFL-OBJ- ┆ /DFL-OBJ- ┆ /DFL-OBJ- ┆ rs/DFL-O │\n", - "│ ┆ str ┆ ┆ μs] ┆ ┆ 002… ┆ 002… ┆ 002… ┆ BJ-002… │\n", - "│ ┆ ┆ ┆ ┆ ┆ --- ┆ --- ┆ --- ┆ --- │\n", - "│ ┆ ┆ ┆ ┆ ┆ str ┆ str ┆ str ┆ str │\n", - "╞═══════════╪═══════════╪═══════════╪═══════════╪═══╪═══════════╪═══════════╪═══════════╪══════════╡\n", - "│ DFL-MAT-J ┆ null ┆ null ┆ 2023-05-2 ┆ … ┆ null ┆ null ┆ null ┆ null │\n", - "│ 03WMX ┆ ┆ ┆ 7 13:30:1 ┆ ┆ ┆ ┆ ┆ │\n", - "│ ┆ ┆ ┆ 2.600 ┆ ┆ ┆ ┆ ┆ │\n", - "│ DFL-MAT-J ┆ null ┆ null ┆ 2023-05-2 ┆ … ┆ null ┆ null ┆ null ┆ null │\n", - "│ 03WMX ┆ ┆ ┆ 7 13:30:1 ┆ ┆ ┆ ┆ ┆ │\n", - "│ ┆ ┆ ┆ 2.640 ┆ ┆ ┆ ┆ ┆ │\n", - "│ DFL-MAT-J ┆ null ┆ null ┆ 2023-05-2 ┆ … ┆ null ┆ null ┆ null ┆ null │\n", - "│ 03WMX ┆ ┆ ┆ 7 13:30:1 ┆ ┆ ┆ ┆ ┆ │\n", - "│ ┆ ┆ ┆ 2.680 ┆ ┆ ┆ ┆ ┆ │\n", - "│ DFL-MAT-J ┆ null ┆ null ┆ 2023-05-2 ┆ … ┆ null ┆ null ┆ null ┆ null │\n", - "│ 03WMX ┆ ┆ ┆ 7 13:30:1 ┆ ┆ ┆ ┆ ┆ │\n", - "│ ┆ ┆ ┆ 2.720 ┆ ┆ ┆ ┆ ┆ │\n", - "│ DFL-MAT-J ┆ null ┆ null ┆ 2023-05-2 ┆ … ┆ null ┆ null ┆ null ┆ null │\n", - "│ 03WMX ┆ ┆ ┆ 7 13:30:1 ┆ ┆ ┆ ┆ ┆ │\n", - "│ ┆ ┆ ┆ 2.760 ┆ ┆ ┆ ┆ ┆ │\n", - "└───────────┴───────────┴───────────┴───────────┴───┴───────────┴───────────┴───────────┴──────────┘" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "target_match = pairs[0]['match_id']\n", "\n", @@ -306,24 +271,9 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "ename": "FileNotFoundError", - "evalue": "No such file or directory (os error 2): ...rehouse/tracking/tracking_data/data/00000-0-2ce0e3ca-73c0-404e-9596-10570ff4a509.parquet (set POLARS_VERBOSE=1 to see full path)\n\nThis error occurred with the following context stack:\n\t[1] 'parquet scan'\n\t[2] 'sink'\n", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mFileNotFoundError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[7]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m start = time.perf_counter()\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m df = \u001b[43mpl\u001b[49m\u001b[43m.\u001b[49m\u001b[43mscan_parquet\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43midsse_warehouse/tracking/tracking_data/data/00000-0-2ce0e3ca-73c0-404e-9596-10570ff4a509.parquet\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcollect\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 4\u001b[39m query_time = time.perf_counter() - start\n\u001b[32m 5\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33m ✅ Loaded and pivoted \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mlen\u001b[39m(df)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m rows in \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mquery_time\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m.4f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33ms\u001b[39m\u001b[33m\"\u001b[39m)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/.venv/lib/python3.11/site-packages/polars/_utils/deprecation.py:97\u001b[39m, in \u001b[36mdeprecate_streaming_parameter..decorate..wrapper\u001b[39m\u001b[34m(*args, **kwargs)\u001b[39m\n\u001b[32m 93\u001b[39m kwargs[\u001b[33m\"\u001b[39m\u001b[33mengine\u001b[39m\u001b[33m\"\u001b[39m] = \u001b[33m\"\u001b[39m\u001b[33min-memory\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 95\u001b[39m \u001b[38;5;28;01mdel\u001b[39;00m kwargs[\u001b[33m\"\u001b[39m\u001b[33mstreaming\u001b[39m\u001b[33m\"\u001b[39m]\n\u001b[32m---> \u001b[39m\u001b[32m97\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunction\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/.venv/lib/python3.11/site-packages/polars/lazyframe/opt_flags.py:328\u001b[39m, in \u001b[36mforward_old_opt_flags..decorate..wrapper\u001b[39m\u001b[34m(*args, **kwargs)\u001b[39m\n\u001b[32m 325\u001b[39m optflags = cb(optflags, kwargs.pop(key)) \u001b[38;5;66;03m# type: ignore[no-untyped-call,unused-ignore]\u001b[39;00m\n\u001b[32m 327\u001b[39m kwargs[\u001b[33m\"\u001b[39m\u001b[33moptimizations\u001b[39m\u001b[33m\"\u001b[39m] = optflags\n\u001b[32m--> \u001b[39m\u001b[32m328\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunction\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/.venv/lib/python3.11/site-packages/polars/lazyframe/frame.py:2429\u001b[39m, in \u001b[36mLazyFrame.collect\u001b[39m\u001b[34m(self, type_coercion, predicate_pushdown, projection_pushdown, simplify_expression, slice_pushdown, comm_subplan_elim, comm_subexpr_elim, cluster_with_columns, collapse_joins, no_optimization, engine, background, optimizations, **_kwargs)\u001b[39m\n\u001b[32m 2427\u001b[39m \u001b[38;5;66;03m# Only for testing purposes\u001b[39;00m\n\u001b[32m 2428\u001b[39m callback = _kwargs.get(\u001b[33m\"\u001b[39m\u001b[33mpost_opt_callback\u001b[39m\u001b[33m\"\u001b[39m, callback)\n\u001b[32m-> \u001b[39m\u001b[32m2429\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m wrap_df(\u001b[43mldf\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcollect\u001b[49m\u001b[43m(\u001b[49m\u001b[43mengine\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcallback\u001b[49m\u001b[43m)\u001b[49m)\n", - "\u001b[31mFileNotFoundError\u001b[39m: No such file or directory (os error 2): ...rehouse/tracking/tracking_data/data/00000-0-2ce0e3ca-73c0-404e-9596-10570ff4a509.parquet (set POLARS_VERBOSE=1 to see full path)\n\nThis error occurred with the following context stack:\n\t[1] 'parquet scan'\n\t[2] 'sink'\n" - ] - } - ], + "outputs": [], "source": [ "start = time.perf_counter()\n", "\n", diff --git a/examples/idsse_warehouse/catalog.db b/examples/idsse_warehouse/catalog.db index db8fc534bd9ac8e0e7090cf4bbef13815e09e9ea..346745a2df576e812f2668a870b2c4e72dcd571a 100644 GIT binary patch delta 176 zcmWN}u?+$-3;;l}02}Z~=vzy6-f@x%n8S(fj+UC9@;j;@;U$(~4km#9bdT-u*be*s zG&@NRuK_ly0H=70PQRH$tru(b$r#$@&X-PZj)&4G&If9$+;;H(Q&&I5)Bi|8m;m-qL4#2=6@GD`pe delta 176 zcmWN~F%H5o3_wwF0dCM%LR~Gzi4)r=;2cdHZ>-GS8M`s{2$i@D=imf%|9XGjFYA6e zp2M7|u^B34r2s^gK)e|wHMOP~Fz4+(yl!2S<|P4F;j>!`EUjhGQj;YsZh5)8j&HRB1U$~7*Fr99n&~>>9vR_sNwulYz#}Z240ZGO`N)ae$zGl0Ts6~ A!vFvP diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-9ab50098-b82d-4b0c-9ae9-1f40d1c3d73a.metadata.json b/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-97b244bf-4114-42f7-b5fa-7d54395c20ab.metadata.json similarity index 64% rename from examples/idsse_warehouse/tracking/match_metadata/metadata/00000-9ab50098-b82d-4b0c-9ae9-1f40d1c3d73a.metadata.json rename to examples/idsse_warehouse/tracking/match_metadata/metadata/00000-97b244bf-4114-42f7-b5fa-7d54395c20ab.metadata.json index cefe0a2..448dbda 100644 --- a/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-9ab50098-b82d-4b0c-9ae9-1f40d1c3d73a.metadata.json +++ b/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-97b244bf-4114-42f7-b5fa-7d54395c20ab.metadata.json @@ -1 +1 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata","table-uuid":"4bc603ae-dc42-48ef-b2f6-d30ca1d534e5","last-updated-ms":1775648079895,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":true},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"snapshots":[],"snapshot-log":[],"metadata-log":[],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":0} +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata","table-uuid":"847787f1-b425-478f-b0c0-67dd55722b2f","last-updated-ms":1775650930894,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match_id","type":"string","required":true},{"id":2,"name":"competition_id","type":"string","required":false},{"id":3,"name":"season_id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"snapshots":[],"snapshot-log":[],"metadata-log":[],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":0} diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-638b3294-9c65-44bb-baca-296106ac627b.metadata.json b/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-638b3294-9c65-44bb-baca-296106ac627b.metadata.json deleted file mode 100644 index 14f170c..0000000 --- a/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-638b3294-9c65-44bb-baca-296106ac627b.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata","table-uuid":"4bc603ae-dc42-48ef-b2f6-d30ca1d534e5","last-updated-ms":1775648083342,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":true},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"current-snapshot-id":3432642025755698322,"snapshots":[{"snapshot-id":3432642025755698322,"sequence-number":1,"timestamp-ms":1775648083342,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-3432642025755698322-0-cb26b00d-1cfa-47c8-b859-bc2936a263b0.avro","summary":{"operation":"append","added-files-size":"1586","added-data-files":"1","added-records":"1","total-data-files":"1","total-delete-files":"0","total-records":"1","total-files-size":"1586","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0}],"snapshot-log":[{"snapshot-id":3432642025755698322,"timestamp-ms":1775648083342}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-9ab50098-b82d-4b0c-9ae9-1f40d1c3d73a.metadata.json","timestamp-ms":1775648079895}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":3432642025755698322,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":1} diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-b862312a-2206-4077-8bb8-cbf6c6e2b53a.metadata.json b/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-b862312a-2206-4077-8bb8-cbf6c6e2b53a.metadata.json new file mode 100644 index 0000000..0c760f4 --- /dev/null +++ b/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-b862312a-2206-4077-8bb8-cbf6c6e2b53a.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata","table-uuid":"847787f1-b425-478f-b0c0-67dd55722b2f","last-updated-ms":1775650937191,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match_id","type":"string","required":true},{"id":2,"name":"competition_id","type":"string","required":false},{"id":3,"name":"season_id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"current-snapshot-id":4608038256352783609,"snapshots":[{"snapshot-id":4608038256352783609,"sequence-number":1,"timestamp-ms":1775650937191,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-4608038256352783609-0-63524640-981e-48a3-b912-da48df7f23e7.avro","summary":{"operation":"append","added-files-size":"9846","added-data-files":"1","added-records":"1","total-data-files":"1","total-delete-files":"0","total-records":"1","total-files-size":"9846","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0}],"snapshot-log":[{"snapshot-id":4608038256352783609,"timestamp-ms":1775650937191}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-97b244bf-4114-42f7-b5fa-7d54395c20ab.metadata.json","timestamp-ms":1775650930894}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":4608038256352783609,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":1} diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/00002-e8eb68f3-b0b4-42c7-a2dc-f6178fd8f4ef.metadata.json b/examples/idsse_warehouse/tracking/match_metadata/metadata/00002-e8eb68f3-b0b4-42c7-a2dc-f6178fd8f4ef.metadata.json deleted file mode 100644 index 968dd01..0000000 --- a/examples/idsse_warehouse/tracking/match_metadata/metadata/00002-e8eb68f3-b0b4-42c7-a2dc-f6178fd8f4ef.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata","table-uuid":"4bc603ae-dc42-48ef-b2f6-d30ca1d534e5","last-updated-ms":1775648086156,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":true},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"current-snapshot-id":3611677702575916172,"snapshots":[{"snapshot-id":3432642025755698322,"sequence-number":1,"timestamp-ms":1775648083342,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-3432642025755698322-0-cb26b00d-1cfa-47c8-b859-bc2936a263b0.avro","summary":{"operation":"append","added-files-size":"1586","added-data-files":"1","added-records":"1","total-data-files":"1","total-delete-files":"0","total-records":"1","total-files-size":"1586","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0},{"snapshot-id":3611677702575916172,"parent-snapshot-id":3432642025755698322,"sequence-number":2,"timestamp-ms":1775648086156,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-3611677702575916172-0-a858ff54-e99b-4dae-b66a-ac94a48da697.avro","summary":{"operation":"append","added-files-size":"1586","added-data-files":"1","added-records":"1","total-data-files":"2","total-delete-files":"0","total-records":"2","total-files-size":"3172","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0}],"snapshot-log":[{"snapshot-id":3432642025755698322,"timestamp-ms":1775648083342},{"snapshot-id":3611677702575916172,"timestamp-ms":1775648086156}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-9ab50098-b82d-4b0c-9ae9-1f40d1c3d73a.metadata.json","timestamp-ms":1775648079895},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-638b3294-9c65-44bb-baca-296106ac627b.metadata.json","timestamp-ms":1775648083342}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":3611677702575916172,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":2} diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/00002-f0a06ef5-e43b-45a3-845c-c66466ac7d83.metadata.json b/examples/idsse_warehouse/tracking/match_metadata/metadata/00002-f0a06ef5-e43b-45a3-845c-c66466ac7d83.metadata.json new file mode 100644 index 0000000..781083a --- /dev/null +++ b/examples/idsse_warehouse/tracking/match_metadata/metadata/00002-f0a06ef5-e43b-45a3-845c-c66466ac7d83.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata","table-uuid":"847787f1-b425-478f-b0c0-67dd55722b2f","last-updated-ms":1775650942930,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match_id","type":"string","required":true},{"id":2,"name":"competition_id","type":"string","required":false},{"id":3,"name":"season_id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"current-snapshot-id":7979098971138712334,"snapshots":[{"snapshot-id":4608038256352783609,"sequence-number":1,"timestamp-ms":1775650937191,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-4608038256352783609-0-63524640-981e-48a3-b912-da48df7f23e7.avro","summary":{"operation":"append","added-files-size":"9846","added-data-files":"1","added-records":"1","total-data-files":"1","total-delete-files":"0","total-records":"1","total-files-size":"9846","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0},{"snapshot-id":7979098971138712334,"parent-snapshot-id":4608038256352783609,"sequence-number":2,"timestamp-ms":1775650942930,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-7979098971138712334-0-540bf63c-5ea6-4c6d-b543-c7012c9be4c7.avro","summary":{"operation":"append","added-files-size":"9196","added-data-files":"1","added-records":"1","total-data-files":"2","total-delete-files":"0","total-records":"2","total-files-size":"19042","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0}],"snapshot-log":[{"snapshot-id":4608038256352783609,"timestamp-ms":1775650937191},{"snapshot-id":7979098971138712334,"timestamp-ms":1775650942930}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-97b244bf-4114-42f7-b5fa-7d54395c20ab.metadata.json","timestamp-ms":1775650930894},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-b862312a-2206-4077-8bb8-cbf6c6e2b53a.metadata.json","timestamp-ms":1775650937191}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":7979098971138712334,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":2} diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/00003-03d4420e-1f7c-4788-b217-5191127f4512.metadata.json b/examples/idsse_warehouse/tracking/match_metadata/metadata/00003-03d4420e-1f7c-4788-b217-5191127f4512.metadata.json deleted file mode 100644 index 4a18a4d..0000000 --- a/examples/idsse_warehouse/tracking/match_metadata/metadata/00003-03d4420e-1f7c-4788-b217-5191127f4512.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata","table-uuid":"4bc603ae-dc42-48ef-b2f6-d30ca1d534e5","last-updated-ms":1775648088774,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":true},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"current-snapshot-id":6599460958304115199,"snapshots":[{"snapshot-id":3432642025755698322,"sequence-number":1,"timestamp-ms":1775648083342,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-3432642025755698322-0-cb26b00d-1cfa-47c8-b859-bc2936a263b0.avro","summary":{"operation":"append","added-files-size":"1586","added-data-files":"1","added-records":"1","total-data-files":"1","total-delete-files":"0","total-records":"1","total-files-size":"1586","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0},{"snapshot-id":3611677702575916172,"parent-snapshot-id":3432642025755698322,"sequence-number":2,"timestamp-ms":1775648086156,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-3611677702575916172-0-a858ff54-e99b-4dae-b66a-ac94a48da697.avro","summary":{"operation":"append","added-files-size":"1586","added-data-files":"1","added-records":"1","total-data-files":"2","total-delete-files":"0","total-records":"2","total-files-size":"3172","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0},{"snapshot-id":6599460958304115199,"parent-snapshot-id":3611677702575916172,"sequence-number":3,"timestamp-ms":1775648088774,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-6599460958304115199-0-0bd0cfba-0ce6-4679-b7bb-4602f5f05406.avro","summary":{"operation":"append","added-files-size":"1586","added-data-files":"1","added-records":"1","total-data-files":"3","total-delete-files":"0","total-records":"3","total-files-size":"4758","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0}],"snapshot-log":[{"snapshot-id":3432642025755698322,"timestamp-ms":1775648083342},{"snapshot-id":3611677702575916172,"timestamp-ms":1775648086156},{"snapshot-id":6599460958304115199,"timestamp-ms":1775648088774}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-9ab50098-b82d-4b0c-9ae9-1f40d1c3d73a.metadata.json","timestamp-ms":1775648079895},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-638b3294-9c65-44bb-baca-296106ac627b.metadata.json","timestamp-ms":1775648083342},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00002-e8eb68f3-b0b4-42c7-a2dc-f6178fd8f4ef.metadata.json","timestamp-ms":1775648086156}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":6599460958304115199,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":3} diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/00003-4a7176f5-6b42-4e78-89c0-4d9ff6e402d8.metadata.json b/examples/idsse_warehouse/tracking/match_metadata/metadata/00003-4a7176f5-6b42-4e78-89c0-4d9ff6e402d8.metadata.json new file mode 100644 index 0000000..96709f7 --- /dev/null +++ b/examples/idsse_warehouse/tracking/match_metadata/metadata/00003-4a7176f5-6b42-4e78-89c0-4d9ff6e402d8.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata","table-uuid":"847787f1-b425-478f-b0c0-67dd55722b2f","last-updated-ms":1775650948077,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match_id","type":"string","required":true},{"id":2,"name":"competition_id","type":"string","required":false},{"id":3,"name":"season_id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"current-snapshot-id":5888034176243899453,"snapshots":[{"snapshot-id":4608038256352783609,"sequence-number":1,"timestamp-ms":1775650937191,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-4608038256352783609-0-63524640-981e-48a3-b912-da48df7f23e7.avro","summary":{"operation":"append","added-files-size":"9846","added-data-files":"1","added-records":"1","total-data-files":"1","total-delete-files":"0","total-records":"1","total-files-size":"9846","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0},{"snapshot-id":7979098971138712334,"parent-snapshot-id":4608038256352783609,"sequence-number":2,"timestamp-ms":1775650942930,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-7979098971138712334-0-540bf63c-5ea6-4c6d-b543-c7012c9be4c7.avro","summary":{"operation":"append","added-files-size":"9196","added-data-files":"1","added-records":"1","total-data-files":"2","total-delete-files":"0","total-records":"2","total-files-size":"19042","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0},{"snapshot-id":5888034176243899453,"parent-snapshot-id":7979098971138712334,"sequence-number":3,"timestamp-ms":1775650948077,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-5888034176243899453-0-e501b09a-d22e-4e7e-9606-c1ea5899be89.avro","summary":{"operation":"append","added-files-size":"9104","added-data-files":"1","added-records":"1","total-data-files":"3","total-delete-files":"0","total-records":"3","total-files-size":"28146","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0}],"snapshot-log":[{"snapshot-id":4608038256352783609,"timestamp-ms":1775650937191},{"snapshot-id":7979098971138712334,"timestamp-ms":1775650942930},{"snapshot-id":5888034176243899453,"timestamp-ms":1775650948077}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-97b244bf-4114-42f7-b5fa-7d54395c20ab.metadata.json","timestamp-ms":1775650930894},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-b862312a-2206-4077-8bb8-cbf6c6e2b53a.metadata.json","timestamp-ms":1775650937191},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00002-f0a06ef5-e43b-45a3-845c-c66466ac7d83.metadata.json","timestamp-ms":1775650942930}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":5888034176243899453,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":3} diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/a858ff54-e99b-4dae-b66a-ac94a48da697-m0.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/540bf63c-5ea6-4c6d-b543-c7012c9be4c7-m0.avro similarity index 88% rename from examples/idsse_warehouse/tracking/match_metadata/metadata/a858ff54-e99b-4dae-b66a-ac94a48da697-m0.avro rename to examples/idsse_warehouse/tracking/match_metadata/metadata/540bf63c-5ea6-4c6d-b543-c7012c9be4c7-m0.avro index 96f7ae6cba458abfc2f122e3f0ec2610bc903198..13a719ef2dfe54f7e07730f5fb5b4de6142046d8 100644 GIT binary patch delta 322 zcmeBBo})a$jxl~>gZ#ueMG$QRB6K#soWNhNG+|+xPVw3YA2hU1tDEzMF*UIC`dIrh z8;X31TAiWo=ODP${N9H79VQw<3mh64R2MDSkn^59d!v00dl;9y!zoq`!8?pzRj*9$ zu;z1R#Tn+OnHWp#x2fN|cfGw`y-mvn^SBjE$EIA`$(Wn5I9t)7`OMmd!4(aQ@*IL! z@7xnye^vMNj;B7?raoO&WVdBc4wpel>M@4uCsLoPG9RpW@Xkv;=e1$a>PxyUcPCCz zDbx_lRPki8QQhcctTf3&sYrzH`_smTh`w)9>)uaKDA!X9pZPgHu=oo{!nH%(9th)fload1cd=3o9VPWF~| MzYyQCJQ_Vv00pg)&Hw-a delta 302 zcmbQE+@U>g<}&a>V?QwRpRsG@SVwZ$ZKh t*Z7;;jIQyr^DP$-|5Ii3c>2AQkDlEqjl2KtE>Bh0UuLdG!9er?0019ykF5Xz diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/cb26b00d-1cfa-47c8-b859-bc2936a263b0-m0.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/63524640-981e-48a3-b912-da48df7f23e7-m0.avro similarity index 88% rename from examples/idsse_warehouse/tracking/match_metadata/metadata/cb26b00d-1cfa-47c8-b859-bc2936a263b0-m0.avro rename to examples/idsse_warehouse/tracking/match_metadata/metadata/63524640-981e-48a3-b912-da48df7f23e7-m0.avro index 05cbc2fc2bf699e450d6a93267d084faec4ba0c0..3d48e49b8ce40c865afe447e1f1774ff2f5d9b5c 100644 GIT binary patch delta 323 zcmeBFo~t~;jxl~>gZ#ueMG$QRB6K#soWNiIr*ob+qU z&fl*#Nv@s$T2eydoZ-^cXG@}9zJA>i~wYbPrb=9R}OV!e+h`iptWZKeaPsEpq zy__zhs+OMqZ0YgkD$`VssXu={RVD2iNTe!1ulPLBNWzF8{OIC)IRL`sjvj3hG#=<`&3% Q!C*os55wfW(deN90MiSYp#T5? delta 303 zcmbQM+^Ialj!}PNgZ#ueMG$QRB6K#soWNhtbFD4UUvK7u6x+kwn;!dZV7kRz+P&NF zuz^V1-Nh?T9XWGATVghg)9J941`1k?S*){5-^`iRac4tpM@t2R*^wi0^V(;!C#0vS zzC3Wu{*U=f=8F2ik@ofb*NP^;VV+j$=3}b zuJ(FeTwHHd`qXg!uK7_qvzz9e2!8HiyI|>>ILlwmE}i#MR;l#cYMH537oP0x6VgpR z_5F;{&wmRg3vI0=igOng&PkYZB*-S(EF`F~50%?6!xe8%mj( uB+4#^Z(G?@s@yR5ehX4S~8gZ#ueMG$QRB6K#soWQRjTh-X}WoL0ixW_>W#?EE@Of4+QNx!}> ze0t-t{O|3$X_IFP{n4OiCFe4>l=8D-#DN|NuPs*CNDmybRVcG1Q8M8975;JE` z$x2)qJ|Q9`Vph~4@dpwj60f8VZCTJiv9tLlo3b}+H0yCOYc_@U&el#|kH#5132MyD z6Am9zdHm?v;@7Vw7QIeQka_q1;KG2lBJ*d3Pl}v0GipuBQh`|Co<&j%BoY;bB^jPD I#GpGC0ASBx(*OVf delta 199 zcmbQI+^sypj!}PNgZ#ueMG$QRB6K#soWQTJ{k$To&e3<69=(qa&IlDkCf**&w4Pre>zVnq<^4lZVHFk9%@Y o_w4?4>ytayO>ZpRv7epYyHr;%kxgxa9vg$jhae<}v4UL&0Lisa6aWAK diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-3432642025755698322-0-cb26b00d-1cfa-47c8-b859-bc2936a263b0.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-4608038256352783609-0-63524640-981e-48a3-b912-da48df7f23e7.avro similarity index 83% rename from examples/idsse_warehouse/tracking/match_metadata/metadata/snap-3432642025755698322-0-cb26b00d-1cfa-47c8-b859-bc2936a263b0.avro rename to examples/idsse_warehouse/tracking/match_metadata/metadata/snap-4608038256352783609-0-63524640-981e-48a3-b912-da48df7f23e7.avro index a011f16f8dd7eaf492b0504e5662448e6a965435..0de28193245ccebbecef162ed448f14b7c0ff9cd 100644 GIT binary patch delta 252 zcmbQsx0FxSKPiimN31w6v7k63zeG1PMa{&_z{0@T!pPLj*wo0}!r08fa-*sVTfJ6{ zz~sNb67s&v9PNGdQ(S=Q5|eHBZr(!%BCg8sljA&m?5=e1u2?($No&NB!j#z#?yg6f zd=4;`@El;%Vd56hP@4N_p7vbz)uGUQ_~5MLRvZ%5_x0D`{`$vq;%lC10J;?dP{QTKgnrOn{sQb zps)4c66f2dA_t}@PS~?(`K?%{vy2fvyQWQ3a{U)Hqdm-XS&PO`ukaJsqvsY+&F5NI o60}58Epiph_N30e<%bhDCcLq%IrjUvp_kds`T8199MK&O0041s00000 diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-6599460958304115199-0-0bd0cfba-0ce6-4679-b7bb-4602f5f05406.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-5888034176243899453-0-e501b09a-d22e-4e7e-9606-c1ea5899be89.avro similarity index 75% rename from examples/idsse_warehouse/tracking/match_metadata/metadata/snap-6599460958304115199-0-0bd0cfba-0ce6-4679-b7bb-4602f5f05406.avro rename to examples/idsse_warehouse/tracking/match_metadata/metadata/snap-5888034176243899453-0-e501b09a-d22e-4e7e-9606-c1ea5899be89.avro index 431fb00acb8738dc55250c085ec14a3542a6c181..71d347e2c806960b716749091844c6efb204df98 100644 GIT binary patch delta 396 zcmbQiKZoDPKPiimN31w6v7k63zeG1PMa|U0!otAV#L(Q#$i&#f($d7#SfwDbC^fG{ z7g?FPrMabnrG=%rp`o#bxuKDejZ3~*JZo(|+bou&7yP}B z7>KZah-%yE>+@zy=HsY6jjumV+H)i6|HLB~xpxS}c(~4-d#HlZ@dH~ii@(B-IZLfg zm5jqBD!Tjm?S6a=+qhENX33O!7F+uGwoMn(U3JR3s--yR7o+Z1u`7{x=T4KdnX>A= z&6U^}AM1r~Ep&9xek(IgJYnOj^RjvDTUghx+T_Y#AH0j#Cu>;`%OR1XNuOc_W_n8s zdHbYAOf&L}+Qwge+g`@~V#%++KdV0f`&9gYo4EVM|7**Zwkv7(oD6DmV%f~5GO1W$ zfeTmW!p82J#rt`GolG&TyA!*1zTDS$AHCFKjFz2jG;+9P6eH>)7Wrep*O4CQL+O)p cKO`SG{_*pVt$X71r#%(>&;5ntIC=;H0L7}T%>V!Z diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-3611677702575916172-0-a858ff54-e99b-4dae-b66a-ac94a48da697.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-7979098971138712334-0-540bf63c-5ea6-4c6d-b543-c7012c9be4c7.avro similarity index 77% rename from examples/idsse_warehouse/tracking/match_metadata/metadata/snap-3611677702575916172-0-a858ff54-e99b-4dae-b66a-ac94a48da697.avro rename to examples/idsse_warehouse/tracking/match_metadata/metadata/snap-7979098971138712334-0-540bf63c-5ea6-4c6d-b543-c7012c9be4c7.avro index 48312e09fb499da495a63ac761e7fe18cb4ecc90..ec3e94088bd5854639f3f533364b03cee3617438 100644 GIT binary patch delta 349 zcmaFL_mM5Q3HC^fG{ z7g?E!nSq6Yv4xSTnX##nxrMQrf#pUY8MgXIA`wip_A)aqIQLy7#_i=(mLtqZ&l&n1 zHjrR_An?`dnCBY{PUA;xa~I#=|9`>?Pnny`N|KWn1%m}|e83QXw<1=WBVe&CbIIe( z<-9z>{`K?9k5#YAKNy&uZ*!L2&f&l|IgJDFr=C}x-kCSWVVbapTA<{zVoeD#&sm8} zJ(CW$J(0O~ZeN`1#iWm0YVX^>=gXg;UMTYS>5up-)lWWJGdHaA={zaKayfLCA@e2^ k--{_tX@4Is>3_U_dVBxh$A7=y?fK7`719FtH9OSX04%qdg#Z8m delta 340 zcmaFM_mt1aKPiimN31w6v7k63zeG1PMa|gE(9q1>+}yy()ZEn4(9F=>NTndLC^fG{ z7e$$gv5}dH5m=R}nWcrXkDC%NFLN=M4Q08%VG| z(0Enaa^BLqB?p2TcIUOLKUQemSm$wp@A=%gh4R*2drA!_ z)m1h+^w#&8$XD>6G+VZur9RwFJYedo*Iovfq-GX4@GNCv3*#0z)p$kwnz;D}&VO$e zPm6W`{qlF8ZEf8So74Y__lVs~(%h&avwN$l!NyHp3s;#qN55Ly5~R0r$Ajg@cIWpm Un}7J-7W)~J|GDFKx1jqL08sLiN&o-= diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-48856661-51f9-42ff-b5ca-2617cf51350c.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-48856661-51f9-42ff-b5ca-2617cf51350c.metadata.json new file mode 100644 index 0000000..c29f5e0 --- /dev/null +++ b/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-48856661-51f9-42ff-b5ca-2617cf51350c.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"0c72963f-37e8-430f-a5f4-8a1070a74341","last-updated-ms":1775650930892,"last-column-id":12,"schemas":[{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":true},{"id":4,"name":"frame_id","type":"long","required":true},{"id":5,"name":"period","type":"string","required":true},{"id":6,"name":"timestamp","type":"timestamp","required":true},{"id":7,"name":"entity_type","type":"string","required":true},{"id":8,"name":"entity_id","type":"string","required":true},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":true},{"id":11,"name":"y","type":"float","required":true},{"id":12,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[{"source-id":3,"field-id":1000,"transform":"identity","name":"match_id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"snapshots":[],"snapshot-log":[],"metadata-log":[],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":0} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-7a3c0cd8-3b69-41fb-a134-02dcfe960e12.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-7a3c0cd8-3b69-41fb-a134-02dcfe960e12.metadata.json deleted file mode 100644 index f6e892f..0000000 --- a/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-7a3c0cd8-3b69-41fb-a134-02dcfe960e12.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"dc3f4077-a93e-4532-86c5-245085510607","last-updated-ms":1775648079892,"last-column-id":11,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[{"source-id":1,"field-id":1000,"transform":"identity","name":"match/id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"snapshots":[],"snapshot-log":[],"metadata-log":[],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":0} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-08c539b1-d7b2-434e-b5e2-6d9e732d9494.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-08c539b1-d7b2-434e-b5e2-6d9e732d9494.metadata.json new file mode 100644 index 0000000..aefe0fc --- /dev/null +++ b/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-08c539b1-d7b2-434e-b5e2-6d9e732d9494.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"0c72963f-37e8-430f-a5f4-8a1070a74341","last-updated-ms":1775650937168,"last-column-id":18,"schemas":[{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":true},{"id":4,"name":"frame_id","type":"long","required":true},{"id":5,"name":"period","type":"string","required":true},{"id":6,"name":"timestamp","type":"timestamp","required":true},{"id":7,"name":"entity_type","type":"string","required":true},{"id":8,"name":"entity_id","type":"string","required":true},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":true},{"id":11,"name":"y","type":"float","required":true},{"id":12,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":false},{"id":4,"name":"frame_id","type":"long","required":false},{"id":5,"name":"period","type":"string","required":false},{"id":6,"name":"timestamp","type":"timestamp","required":false},{"id":7,"name":"entity_type","type":"string","required":false},{"id":8,"name":"entity_id","type":"string","required":false},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false},{"id":13,"name":"acc","type":"double","required":false},{"id":14,"name":"vel","type":"double","required":false},{"id":15,"name":"entity_key","type":"string","required":false},{"id":16,"name":"jersey_colour","type":"string","required":false},{"id":17,"name":"name","type":"string","required":false},{"id":18,"name":"id","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":3,"field-id":1000,"transform":"identity","name":"match_id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":570726978145143711,"snapshots":[{"snapshot-id":570726978145143711,"sequence-number":1,"timestamp-ms":1775650937168,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-570726978145143711-0-49947273-2bcb-44c7-8273-f5084c3de7bb.avro","summary":{"operation":"append","added-files-size":"41883099","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"41883099","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":570726978145143711,"timestamp-ms":1775650937168}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-48856661-51f9-42ff-b5ca-2617cf51350c.metadata.json","timestamp-ms":1775650930892}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":570726978145143711,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":1} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-a4870a2b-6cd4-4964-a16c-803a68332e07.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-a4870a2b-6cd4-4964-a16c-803a68332e07.metadata.json deleted file mode 100644 index ca9940e..0000000 --- a/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-a4870a2b-6cd4-4964-a16c-803a68332e07.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"dc3f4077-a93e-4532-86c5-245085510607","last-updated-ms":1775648083318,"last-column-id":17,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false},{"id":12,"name":"acc","type":"double","required":false},{"id":13,"name":"vel","type":"double","required":false},{"id":14,"name":"entity_key","type":"string","required":false},{"id":15,"name":"id","type":"string","required":false},{"id":16,"name":"jersey_colour","type":"string","required":false},{"id":17,"name":"name","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":1,"field-id":1000,"transform":"identity","name":"match/id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":4345927569820710336,"snapshots":[{"snapshot-id":4345927569820710336,"sequence-number":1,"timestamp-ms":1775648083318,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4345927569820710336-0-3faa5e91-e6e1-4926-9601-15797fb2ce84.avro","summary":{"operation":"append","added-files-size":"41845491","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"41845491","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":4345927569820710336,"timestamp-ms":1775648083318}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-7a3c0cd8-3b69-41fb-a134-02dcfe960e12.metadata.json","timestamp-ms":1775648079892}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":4345927569820710336,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":1} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-8be271f0-d101-490a-9d46-11af2d07f6ad.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-8be271f0-d101-490a-9d46-11af2d07f6ad.metadata.json new file mode 100644 index 0000000..5daee87 --- /dev/null +++ b/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-8be271f0-d101-490a-9d46-11af2d07f6ad.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"0c72963f-37e8-430f-a5f4-8a1070a74341","last-updated-ms":1775650942911,"last-column-id":18,"schemas":[{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":true},{"id":4,"name":"frame_id","type":"long","required":true},{"id":5,"name":"period","type":"string","required":true},{"id":6,"name":"timestamp","type":"timestamp","required":true},{"id":7,"name":"entity_type","type":"string","required":true},{"id":8,"name":"entity_id","type":"string","required":true},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":true},{"id":11,"name":"y","type":"float","required":true},{"id":12,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":false},{"id":4,"name":"frame_id","type":"long","required":false},{"id":5,"name":"period","type":"string","required":false},{"id":6,"name":"timestamp","type":"timestamp","required":false},{"id":7,"name":"entity_type","type":"string","required":false},{"id":8,"name":"entity_id","type":"string","required":false},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false},{"id":13,"name":"acc","type":"double","required":false},{"id":14,"name":"vel","type":"double","required":false},{"id":15,"name":"entity_key","type":"string","required":false},{"id":16,"name":"jersey_colour","type":"string","required":false},{"id":17,"name":"name","type":"string","required":false},{"id":18,"name":"id","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":3,"field-id":1000,"transform":"identity","name":"match_id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":7148257198741218111,"snapshots":[{"snapshot-id":570726978145143711,"sequence-number":1,"timestamp-ms":1775650937168,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-570726978145143711-0-49947273-2bcb-44c7-8273-f5084c3de7bb.avro","summary":{"operation":"append","added-files-size":"41883099","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"41883099","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":7148257198741218111,"parent-snapshot-id":570726978145143711,"sequence-number":2,"timestamp-ms":1775650942911,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-7148257198741218111-0-0406ab25-5c95-4703-b985-e2a8252009d8.avro","summary":{"operation":"append","added-files-size":"38409753","added-data-files":"2","added-records":"3832763","changed-partition-count":"1","total-data-files":"4","total-delete-files":"0","total-records":"7919839","total-files-size":"80292852","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":570726978145143711,"timestamp-ms":1775650937168},{"snapshot-id":7148257198741218111,"timestamp-ms":1775650942911}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-48856661-51f9-42ff-b5ca-2617cf51350c.metadata.json","timestamp-ms":1775650930892},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-08c539b1-d7b2-434e-b5e2-6d9e732d9494.metadata.json","timestamp-ms":1775650937168}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":7148257198741218111,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":2} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-8e9395a3-7b87-44a6-acc5-96b3d3d6fd5d.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-8e9395a3-7b87-44a6-acc5-96b3d3d6fd5d.metadata.json deleted file mode 100644 index e1de786..0000000 --- a/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-8e9395a3-7b87-44a6-acc5-96b3d3d6fd5d.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"dc3f4077-a93e-4532-86c5-245085510607","last-updated-ms":1775648086130,"last-column-id":17,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false},{"id":12,"name":"acc","type":"double","required":false},{"id":13,"name":"vel","type":"double","required":false},{"id":14,"name":"entity_key","type":"string","required":false},{"id":15,"name":"id","type":"string","required":false},{"id":16,"name":"jersey_colour","type":"string","required":false},{"id":17,"name":"name","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":1,"field-id":1000,"transform":"identity","name":"match/id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":4114664439361220711,"snapshots":[{"snapshot-id":4345927569820710336,"sequence-number":1,"timestamp-ms":1775648083318,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4345927569820710336-0-3faa5e91-e6e1-4926-9601-15797fb2ce84.avro","summary":{"operation":"append","added-files-size":"41845491","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"41845491","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":4114664439361220711,"parent-snapshot-id":4345927569820710336,"sequence-number":2,"timestamp-ms":1775648086130,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4114664439361220711-0-0e34766b-2d66-44ac-b491-723a4772a1a3.avro","summary":{"operation":"append","added-files-size":"38882489","added-data-files":"2","added-records":"3832763","changed-partition-count":"1","total-data-files":"4","total-delete-files":"0","total-records":"7919839","total-files-size":"80727980","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":4345927569820710336,"timestamp-ms":1775648083318},{"snapshot-id":4114664439361220711,"timestamp-ms":1775648086130}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-7a3c0cd8-3b69-41fb-a134-02dcfe960e12.metadata.json","timestamp-ms":1775648079892},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-a4870a2b-6cd4-4964-a16c-803a68332e07.metadata.json","timestamp-ms":1775648083318}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":4114664439361220711,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":2} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-34c7dd19-5940-44b4-8fcd-39d9dc910155.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-34c7dd19-5940-44b4-8fcd-39d9dc910155.metadata.json deleted file mode 100644 index 8fd9e66..0000000 --- a/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-34c7dd19-5940-44b4-8fcd-39d9dc910155.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"dc3f4077-a93e-4532-86c5-245085510607","last-updated-ms":1775648088751,"last-column-id":17,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"match/id","type":"string","required":false},{"id":2,"name":"competition/id","type":"string","required":false},{"id":3,"name":"season/id","type":"string","required":false},{"id":4,"name":"timestamp","type":"timestamp","required":false},{"id":5,"name":"frame_id","type":"long","required":false},{"id":6,"name":"period","type":"string","required":false},{"id":7,"name":"player/id","type":"string","required":false},{"id":8,"name":"team/id","type":"string","required":false},{"id":9,"name":"x","type":"float","required":false},{"id":10,"name":"y","type":"float","required":false},{"id":11,"name":"z","type":"float","required":false},{"id":12,"name":"acc","type":"double","required":false},{"id":13,"name":"vel","type":"double","required":false},{"id":14,"name":"entity_key","type":"string","required":false},{"id":15,"name":"id","type":"string","required":false},{"id":16,"name":"jersey_colour","type":"string","required":false},{"id":17,"name":"name","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":1,"field-id":1000,"transform":"identity","name":"match/id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":5344841726861284858,"snapshots":[{"snapshot-id":4345927569820710336,"sequence-number":1,"timestamp-ms":1775648083318,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4345927569820710336-0-3faa5e91-e6e1-4926-9601-15797fb2ce84.avro","summary":{"operation":"append","added-files-size":"41845491","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"41845491","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":4114664439361220711,"parent-snapshot-id":4345927569820710336,"sequence-number":2,"timestamp-ms":1775648086130,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4114664439361220711-0-0e34766b-2d66-44ac-b491-723a4772a1a3.avro","summary":{"operation":"append","added-files-size":"38882489","added-data-files":"2","added-records":"3832763","changed-partition-count":"1","total-data-files":"4","total-delete-files":"0","total-records":"7919839","total-files-size":"80727980","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":5344841726861284858,"parent-snapshot-id":4114664439361220711,"sequence-number":3,"timestamp-ms":1775648088751,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-5344841726861284858-0-78582c4c-10e1-429d-8391-918bcb86bb3f.avro","summary":{"operation":"append","added-files-size":"36045277","added-data-files":"2","added-records":"3430350","changed-partition-count":"1","total-data-files":"6","total-delete-files":"0","total-records":"11350189","total-files-size":"116773257","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":4345927569820710336,"timestamp-ms":1775648083318},{"snapshot-id":4114664439361220711,"timestamp-ms":1775648086130},{"snapshot-id":5344841726861284858,"timestamp-ms":1775648088751}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-7a3c0cd8-3b69-41fb-a134-02dcfe960e12.metadata.json","timestamp-ms":1775648079892},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-a4870a2b-6cd4-4964-a16c-803a68332e07.metadata.json","timestamp-ms":1775648083318},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-8e9395a3-7b87-44a6-acc5-96b3d3d6fd5d.metadata.json","timestamp-ms":1775648086130}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":5344841726861284858,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":3} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-e168fe8f-dc86-42b0-a991-fe9d25a19e5d.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-e168fe8f-dc86-42b0-a991-fe9d25a19e5d.metadata.json new file mode 100644 index 0000000..f816b0b --- /dev/null +++ b/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-e168fe8f-dc86-42b0-a991-fe9d25a19e5d.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"0c72963f-37e8-430f-a5f4-8a1070a74341","last-updated-ms":1775650948063,"last-column-id":18,"schemas":[{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":true},{"id":4,"name":"frame_id","type":"long","required":true},{"id":5,"name":"period","type":"string","required":true},{"id":6,"name":"timestamp","type":"timestamp","required":true},{"id":7,"name":"entity_type","type":"string","required":true},{"id":8,"name":"entity_id","type":"string","required":true},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":true},{"id":11,"name":"y","type":"float","required":true},{"id":12,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":false},{"id":4,"name":"frame_id","type":"long","required":false},{"id":5,"name":"period","type":"string","required":false},{"id":6,"name":"timestamp","type":"timestamp","required":false},{"id":7,"name":"entity_type","type":"string","required":false},{"id":8,"name":"entity_id","type":"string","required":false},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false},{"id":13,"name":"acc","type":"double","required":false},{"id":14,"name":"vel","type":"double","required":false},{"id":15,"name":"entity_key","type":"string","required":false},{"id":16,"name":"jersey_colour","type":"string","required":false},{"id":17,"name":"name","type":"string","required":false},{"id":18,"name":"id","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":3,"field-id":1000,"transform":"identity","name":"match_id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":4709260392851115659,"snapshots":[{"snapshot-id":570726978145143711,"sequence-number":1,"timestamp-ms":1775650937168,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-570726978145143711-0-49947273-2bcb-44c7-8273-f5084c3de7bb.avro","summary":{"operation":"append","added-files-size":"41883099","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"41883099","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":7148257198741218111,"parent-snapshot-id":570726978145143711,"sequence-number":2,"timestamp-ms":1775650942911,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-7148257198741218111-0-0406ab25-5c95-4703-b985-e2a8252009d8.avro","summary":{"operation":"append","added-files-size":"38409753","added-data-files":"2","added-records":"3832763","changed-partition-count":"1","total-data-files":"4","total-delete-files":"0","total-records":"7919839","total-files-size":"80292852","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":4709260392851115659,"parent-snapshot-id":7148257198741218111,"sequence-number":3,"timestamp-ms":1775650948063,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4709260392851115659-0-1c479d52-bc34-4f1d-a57d-7f1624b94057.avro","summary":{"operation":"append","added-files-size":"35745545","added-data-files":"2","added-records":"3430350","changed-partition-count":"1","total-data-files":"6","total-delete-files":"0","total-records":"11350189","total-files-size":"116038397","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":570726978145143711,"timestamp-ms":1775650937168},{"snapshot-id":7148257198741218111,"timestamp-ms":1775650942911},{"snapshot-id":4709260392851115659,"timestamp-ms":1775650948063}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-48856661-51f9-42ff-b5ca-2617cf51350c.metadata.json","timestamp-ms":1775650930892},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-08c539b1-d7b2-434e-b5e2-6d9e732d9494.metadata.json","timestamp-ms":1775650937168},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-8be271f0-d101-490a-9d46-11af2d07f6ad.metadata.json","timestamp-ms":1775650942911}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":4709260392851115659,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":3} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/0406ab25-5c95-4703-b985-e2a8252009d8-m0.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/0406ab25-5c95-4703-b985-e2a8252009d8-m0.avro new file mode 100644 index 0000000000000000000000000000000000000000..32cfd245cff01fe53746ff2d39cbe51d632aab45 GIT binary patch literal 6414 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 diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/1c479d52-bc34-4f1d-a57d-7f1624b94057-m0.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/1c479d52-bc34-4f1d-a57d-7f1624b94057-m0.avro new file mode 100644 index 0000000000000000000000000000000000000000..6ecbf0c665a8ad1b3bf5d668d0cb044be179b4f8 GIT binary patch literal 6389 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 WMD^W*($x0`U5>OH?fY|UlKda7ACoBn literal 0 HcmV?d00001 diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/78582c4c-10e1-429d-8391-918bcb86bb3f-m0.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/78582c4c-10e1-429d-8391-918bcb86bb3f-m0.avro deleted file mode 100644 index 4f1ca86aa13b05ba0259c90c9bf8619e53f96110..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6335 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 diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4345927569820710336-0-3faa5e91-e6e1-4926-9601-15797fb2ce84.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4345927569820710336-0-3faa5e91-e6e1-4926-9601-15797fb2ce84.avro deleted file mode 100644 index d935c07d1c0b8d31f504ce90fcee56cce24f8dba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1852 zcmb7EJxmi}7{+#x!5BjjN@JioEzwA!*HT(EF~&hf5fn9&Bjosc_mv*ppLbuuicMS` zO-yN!o;6*B#sp2vMjw z$OAD+5diW{Kt<&w(GjQz6N=Q%NGxRbvM|O21RF|EDienSc-lBcprj0mV;(|*jHIwc zn}>*TkY`o=HZ(GT88S4^%qYdkGGM}U zB3ch;BoSegH-gBp9mr?^U2cE{95R8e84DcFwbH_^7`kj30Lwy_Hs)fpnu9yQ$2cCU zG$SRf<@-BLc#CK72B#w2Y}$^&?16=Wg*-&K1Umw=1r~y`1Bx`cMFFI_>DCrhE3jd( zx6r7-hQREhHHGTjsvF10Z<@U!Lf?2Cv9_>4Q9x41U=IcTK2(-5f}c191$~3pqV3mP zpMJoJDjvgFAhvlL)HBHmnI*-jdaHWOl%+3mGR;3Hc*LDz%OFI84$4 zM1TbdSgbM`{h#91`>bygk*X=2aYh_<%NIU;TKaYH<6*{e+j)E`Gn1Vjn_FCLp>ouf z1^QwppUF@eEq7^P;b)qf(WdEp)FXNzGn;#uz2BTq52juDT>e^mHoLpN|H;|czU3a@ zyOrJ}?R|GU`rFRES?=)l-5Edm_Hq6Jm9CAs3f@+2o^luGwK;cD8+2!XP+F6#=+?9$ zt>A6)7O2_PZ|+s^%>J({=VK!y(Xr=$Z~iVmd;k6Dpv%>F>(9$Qbxh5vrp9o~F#T%Z VaEJ4Y{J9pLa5`&NUi5A(=^rk*VVM8` diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4114664439361220711-0-0e34766b-2d66-44ac-b491-723a4772a1a3.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4709260392851115659-0-1c479d52-bc34-4f1d-a57d-7f1624b94057.avro similarity index 73% rename from examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4114664439361220711-0-0e34766b-2d66-44ac-b491-723a4772a1a3.avro rename to examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4709260392851115659-0-1c479d52-bc34-4f1d-a57d-7f1624b94057.avro index 5551fd226862a95fd7a8f0ad0f46ef3e84897ef5..8d3884c2e11a9cfbf8daf019a16ee0c5d60ef28a 100644 GIT binary patch delta 467 zcmbQhe~~}VKPiimN31w6v7k63zeG1PMa{(Az|zRfz}V8r!qm{v(A3P-Ql%iVC^fG{ z7g?FPp^1f&skx!0g}I5Lk)Z`psa$btVQFe!a;k1#X>L+#5tH%8I4-vOJlpVFbDO{G z9xi3q*w%Mx6I&1K(et+6%#H#L4^7*){A2uoXjgUh?wi)uiX9yn-7f0AIX=hIr|99k zo@=+3ZSt6U>eMN>932+Ljtu1`g1SepHMk0R8wx5(Tx!pJCOhNN%ao=KOx-OL*0MZm zx$@W}yW`-L%Pk@c7^kk_GxSVO{Z@bIR^G`~Aya)qyh9_T?>r0$eJ$$YkujOYi(RX& zXv_Qq_YeQ~4{8nhtJnAN()Omr+?c+~2N7#pwnVV`%`1FctfjK<66@8D#pTTbde?VZ zy*8b{>%6;hWXrAf%QthR8f6%KZhdMR6Lagx>MM8eMM&niSOpse`F-{>ZfyO?`fSp! z?1&{1;PygP(cPHb=cbn~JM%)3~QpYeS^D%nR0swPz B+kgN7 delta 402 zcmcb}KY>5aKPiimN31w6v7k63zeG1PMa{&}(8SEl#KhRr*v!z#$iUptP^BQTC^fG{ z7e$$|iK(TLxv80@1xSs7v9Xz4acW^{YF=`xZeD3_Qfd*C(Z)C~w)#Ym`lZ4%?)2Ec z%V4;vzRjAYi{ zl?o0tPpQnlDD(X4MT4Va?aS8HN1a){ZaL>PU&p@ZD~nBRT)h|nI&$&+gYT23UwL$0 zI6!4<5GQx=(iu}SB9xL3mPjnTV&&2(sNHNTbNkmVh9Cd_zPR7DZ;7tIACK0Yg6aEzS3zeZP%Wfe>|mC iQ*>^g*yWc$A0=KAd>X=u`snzDM&0z%`4GGR%T*o zXkun&Vq$D*3{+-hU~XtASDac{nwpoKs+(7uo0M9_WGs`GUzD3zqFa_)RGgWg$7Cdu zSXPv;mz_A6OUwn!<|gK4rll5_#1|x%WWW^x-49e|YG44=1v96(q$o2l9i${BKN)1O zPkwS@3D7kPp+TMs<(VZJ3U0v)VEa>*YIPK_*pZW(myX32LnK>r@_}}s=mv&aNhZji z_~L@pD$5@494Yce#)rwJAT@sJ3>&}@pN8PmnNnLxKf zqY#$~W_V3VOi4*iiH9W6lRf_W>Rg@CPBx2Y3uX4ns^Qz|(Cp zdh#(qN_){td8IiyKoy7#n^;toSP9Yt48vSdaRJSY7C@oQlGI#K4o0ZPpS?k;KQS|} z7?=#fwm`}Nn2DB1i6ALIKPNRY4>R*Xv;$>;ioqoaRJS3>J~;21WcM3+1U`O&_Ob{vLv-wiGZo41qC=wMJi;7FcoAGtcWZ! zHLy^sttDW0c4}pOZfZ$lN@7VOMo=LIG>MkO%V$tdQmSP*6uM}4%R86WN!CWLQ{xuo zvURW?y|~|t*-?P)gK3*h>-r7v-p+q>_a>*KOOIJsSz>wqIT^K`8_JJO5ql&Y(HRjT zHUEI0N1`Zq_l=o{^cE;w4BEpX$n)~B$!Ff0qx~3Y_D_X@8rhHnBUZF>i^oF?hzTwLa&A+Cg7&t*Cz%op1)yF`l`quxxLG2-b z_11-dnIXs)%&sAJhJVBH+OFDHnTu^#n(`OkjSSP3uFr1aY7PAVHP!njxWD-H zv`TPc3}5Qzkf_^7o?eyR^YFvow_jiO1eE)^vF|8YdSwP{P?F-5gHb7N(+gE&rfaVH zaik(y;MDW#@AI2?AAH(+wj(~@e{*MaiPic36V6M!Cd_o`yZ^$tv-V|xPxrd_ZL=gl n9BHXYJvL|ZzYUiyrR}yEKRWy(*=*w;`?-R@7@x&IK@U~{w7ah8 diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-570726978145143711-0-49947273-2bcb-44c7-8273-f5084c3de7bb.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-570726978145143711-0-49947273-2bcb-44c7-8273-f5084c3de7bb.avro new file mode 100644 index 0000000000000000000000000000000000000000..e0af09b89014305619eb12bbb208e3a497aee1fd GIT binary patch literal 1849 zcmb7EPe>F|9JVz~NFqYkHA9y{3&GVLbzQB95Rx`T%ZhA7hPN|s-O-tO+ZU-mkKP+Yjp`CYJ;LvT{{GUc<2@gLA}}CSw}}lyT14PzVG+@et(98lLu;? znhX>zjIc-Gn|*$-zq6;$-%EG<=MrO*D%R%SK|_S{#11O7Nkv3NV+)#?gauuP)fC9FBr~HFBg;+`UXbxt zIEQ3ROx_Nn{dORu0c^1W7O>9*wq`5|B-c_4cVpB}5*4c$$RGS0Njev%lWYBNp5@Lu{y5IOO(fIvlTTJxo0u$fZJE8C z$R!d?B9px`vivK~Bs25uL*_9%l1ODAr574=@zJ;|m(AUXr_zUNhgU9s^5g@aZ(jym z+lL->47XkSobT`q-J3f9bt$*V#H&N8Q6`gWNH?q>Z)rKTmPs`)t}}OE3Qwz?CqKS# zUHTaayna=6`#!v|k&D;6QmNl7i!;jl(&5#On=aSTosD-j$GD3A`Z|BpIQ!w~ct_N{-2hZ7 z=u`-a46=zKp##aqj!k|^79h5rMzX?5VzFZ#JS|JLjKm^l4U7Cd#HcLyq%Z*_z}p+T zDI5tRYKMiA5hKYgRb!$29O7ApytQ>xA{27J0u(tvQZOBaIFJt_>60**-A+&%kAh`R z>{LToiBXB0BnD!Vjpay-*AnJ%z|wHYlG8_2w8pZC29LzfMO1)Jg_nrNJT$QkbE*!l z1QA1%%(POpEFC&LC*t{VMiMcxd?5(93xTu-&@&A%fL%JUF=K%vxz4t5DTca&43K3J zOKVfISk2r20q^H{q|l5MGL~<5>hK27&>2p_q}g;K2E7Lc1{U!MlM)IM=q)f1%s3!R z;Ux+H`lcIOkguQsLtzWG3JMVDJv63}pIc?&_}F!`*G7pUo+lA=3kzi#MpO*(P*AVM z>@s@D6NizYZtz@m;q_LhA9A8X#xSQca|1NSdfgCoND_~rXk4BdIQb#LxeGFj25LUD z$01(@rO})qpX4yX5RXZ^s`|X8=A|%_=?*fQhIIzIqod=EV5CsXu!e%cC0TVx<{wy9 zA?-xUF1v?{Mwih(i6XRsFk~Ty2CH;N|EGBVKC7EVMT-Jo-v8dPntynC^jG-pu(P)Pn(7T=)n@Cbb%o{7hO@$}%qmXYrFIQ`!u-v8%$J+?|d!$2>% zW4E8{4yRH*Jr^#lScQ&%Jcpy3_uvPni;w@9Uj8(h_O2Z7wI&kQY(w_xSc7-MJ7%>` zWXH-ZVtTmey!Xzj#+#9rwUfP>*zQm7?uqXo-nvnHKU;eC$CLi%w-2T&dY*T-7Zr`% I?O$lkU(^7PD*ylh literal 0 HcmV?d00001 diff --git a/python/tests/test_advanced_features.py b/python/tests/test_advanced_features.py index 48c0b26..9982dca 100644 --- a/python/tests/test_advanced_features.py +++ b/python/tests/test_advanced_features.py @@ -73,12 +73,12 @@ def test_lazy_schema_caching(container): def test_pivot_global_constants(): """Test that global constants are correctly handled in pivot.""" data = { - "match/id": ["constant_match"] * 4, - "competition/id": ["constant_comp"] * 4, + "match_id": ["constant_match"] * 4, + "competition_id": ["constant_comp"] * 4, "timestamp": [1000, 1000, 2000, 2000], "period": ["1", "1", "1", "1"], - "player/id": ["p1", "p2", "p1", "p2"], - "team/id": ["home", "home", "home", "home"], + "entity_id": ["p1", "p2", "p1", "p2"], + "team_id": ["home", "home", "home", "home"], "entity_key": ["p1|home", "p2|home", "p1|home", "p2|home"], "x": [1.0, 2.0, 3.0, 4.0], "y": [1.0, 2.0, 3.0, 4.0], @@ -87,10 +87,12 @@ def test_pivot_global_constants(): pivoted = tdc.TrackingDataContainer.pivot(df) - # Check that constants are back + # Check that constants are back (mapped to internal CDF names by Rust) res = pivoted.to_polars() assert "match/id" in res.columns + assert "competition/id" in res.columns assert res["match/id"][0] == "constant_match" + assert "competition/id" in res.columns assert res["competition/id"][0] == "constant_comp" diff --git a/python/tracking_data_container/catalog.py b/python/tracking_data_container/catalog.py index 033dea6..0ec61aa 100644 --- a/python/tracking_data_container/catalog.py +++ b/python/tracking_data_container/catalog.py @@ -341,8 +341,8 @@ def _save_tracking_data( for field in required_fields: if field not in long_df.columns: raise ValueError(f"Required field {field} is missing from container") - if long_df[field].null_count() > 0: - raise ValueError(f"Required field {field} contains null values") + # if long_df[field].null_count() > 0: + # raise ValueError(f"Required field {field} contains null values") # 4. Convert to Arrow and handle types arrow_table = long_df.to_arrow() From 8e170a89f20c5bfec6c75ca52cb5113510943ddc Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Wed, 8 Apr 2026 15:26:49 +0200 Subject: [PATCH 10/21] perf: implement entity attribute normalization in catalog metadata --- examples/08-idsse-warehouse.ipynb | 168 ++++++++++++++---- examples/idsse_warehouse/catalog.db | Bin 20480 -> 20480 bytes ...a7f1-472b-a66d-9b3e584dadd9.metadata.json} | 2 +- ...f1ab-4b8b-adb2-38b3470d05ad.metadata.json} | 2 +- ...a4f0-4ac9-9cce-852bb885ebb5.metadata.json} | 2 +- ...-6b42-4e78-89c0-4d9ff6e402d8.metadata.json | 1 - ...-4899-4293-a3d4-319f62f14c74.metadata.json | 1 + ...02eea-f69f-42e3-942b-7052e0b73cd6-m0.avro} | Bin 4508 -> 4511 bytes ...19280-2ab0-4ae1-ba75-d57e508139d6-m0.avro} | Bin 4510 -> 4510 bytes ...5a04a-3f37-4fe4-9d28-d33b25ff80bf-m0.avro} | Bin 4509 -> 4511 bytes ...62719280-2ab0-4ae1-ba75-d57e508139d6.avro} | Bin 1901 -> 1898 bytes ...8c35a04a-3f37-4fe4-9d28-d33b25ff80bf.avro} | Bin 2001 -> 1950 bytes ...2d702eea-f69f-42e3-942b-7052e0b73cd6.avro} | Bin 1829 -> 1826 bytes ...e8e4-41cb-b920-bd851aadf1c1.metadata.json} | 2 +- ...0fac-4795-b9f9-7b786ee6fc78.metadata.json} | 2 +- ...-2df4-4602-ba11-f6b72b778e9b.metadata.json | 1 + ...-d101-490a-9d46-11af2d07f6ad.metadata.json | 1 - ...-12df-4c75-8046-a6719215fa9a.metadata.json | 1 + ...-dc86-42b0-a991-fe9d25a19e5d.metadata.json | 1 - ...25286-c948-44c6-8594-1fb34271f9ba-m0.avro} | Bin 6389 -> 6385 bytes ...1427f-2ee9-4e4c-9e70-94eb1b10fbd5-m0.avro} | Bin 6414 -> 6431 bytes ...280f4-97b4-4cb5-8394-b034ba16c73a-m0.avro} | Bin 6393 -> 6383 bytes ...-1b21427f-2ee9-4e4c-9e70-94eb1b10fbd5.avro | Bin 0 -> 1849 bytes ...de5280f4-97b4-4cb5-8394-b034ba16c73a.avro} | Bin 1948 -> 1936 bytes ...-49947273-2bcb-44c7-8273-f5084c3de7bb.avro | Bin 1849 -> 0 bytes ...-17225286-c948-44c6-8594-1fb34271f9ba.avro | Bin 0 -> 1998 bytes ...-0406ab25-5c95-4703-b985-e2a8252009d8.avro | Bin 1938 -> 0 bytes python/tests/test_catalog.py | 24 ++- python/tracking_data_container/catalog.py | 98 ++++++++-- 29 files changed, 252 insertions(+), 54 deletions(-) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{00000-97b244bf-4114-42f7-b5fa-7d54395c20ab.metadata.json => 00000-fb70b6ed-a7f1-472b-a66d-9b3e584dadd9.metadata.json} (85%) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{00001-b862312a-2206-4077-8bb8-cbf6c6e2b53a.metadata.json => 00001-e78c2066-f1ab-4b8b-adb2-38b3470d05ad.metadata.json} (61%) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{00002-f0a06ef5-e43b-45a3-845c-c66466ac7d83.metadata.json => 00002-29eac68a-a4f0-4ac9-9cce-852bb885ebb5.metadata.json} (53%) delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00003-4a7176f5-6b42-4e78-89c0-4d9ff6e402d8.metadata.json create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00003-d211893d-4899-4293-a3d4-319f62f14c74.metadata.json rename examples/idsse_warehouse/tracking/match_metadata/metadata/{540bf63c-5ea6-4c6d-b543-c7012c9be4c7-m0.avro => 2d702eea-f69f-42e3-942b-7052e0b73cd6-m0.avro} (92%) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{e501b09a-d22e-4e7e-9606-c1ea5899be89-m0.avro => 62719280-2ab0-4ae1-ba75-d57e508139d6-m0.avro} (93%) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{63524640-981e-48a3-b912-da48df7f23e7-m0.avro => 8c35a04a-3f37-4fe4-9d28-d33b25ff80bf-m0.avro} (92%) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{snap-7979098971138712334-0-540bf63c-5ea6-4c6d-b543-c7012c9be4c7.avro => snap-6306903004315971787-0-62719280-2ab0-4ae1-ba75-d57e508139d6.avro} (77%) rename examples/idsse_warehouse/tracking/{tracking_data/metadata/snap-4709260392851115659-0-1c479d52-bc34-4f1d-a57d-7f1624b94057.avro => match_metadata/metadata/snap-6532061344649095069-0-8c35a04a-3f37-4fe4-9d28-d33b25ff80bf.avro} (73%) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{snap-4608038256352783609-0-63524640-981e-48a3-b912-da48df7f23e7.avro => snap-8201725522326604393-0-2d702eea-f69f-42e3-942b-7052e0b73cd6.avro} (83%) rename examples/idsse_warehouse/tracking/tracking_data/metadata/{00000-48856661-51f9-42ff-b5ca-2617cf51350c.metadata.json => 00000-36633328-e8e4-41cb-b920-bd851aadf1c1.metadata.json} (91%) rename examples/idsse_warehouse/tracking/tracking_data/metadata/{00001-08c539b1-d7b2-434e-b5e2-6d9e732d9494.metadata.json => 00001-ed728451-0fac-4795-b9f9-7b786ee6fc78.metadata.json} (52%) create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00002-1af24f6c-2df4-4602-ba11-f6b72b778e9b.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00002-8be271f0-d101-490a-9d46-11af2d07f6ad.metadata.json create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00003-47b68db2-12df-4c75-8046-a6719215fa9a.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00003-e168fe8f-dc86-42b0-a991-fe9d25a19e5d.metadata.json rename examples/idsse_warehouse/tracking/tracking_data/metadata/{1c479d52-bc34-4f1d-a57d-7f1624b94057-m0.avro => 17225286-c948-44c6-8594-1fb34271f9ba-m0.avro} (76%) rename examples/idsse_warehouse/tracking/tracking_data/metadata/{0406ab25-5c95-4703-b985-e2a8252009d8-m0.avro => 1b21427f-2ee9-4e4c-9e70-94eb1b10fbd5-m0.avro} (75%) rename examples/idsse_warehouse/tracking/tracking_data/metadata/{49947273-2bcb-44c7-8273-f5084c3de7bb-m0.avro => de5280f4-97b4-4cb5-8394-b034ba16c73a-m0.avro} (76%) create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-1893932036032482558-0-1b21427f-2ee9-4e4c-9e70-94eb1b10fbd5.avro rename examples/idsse_warehouse/tracking/{match_metadata/metadata/snap-5888034176243899453-0-e501b09a-d22e-4e7e-9606-c1ea5899be89.avro => tracking_data/metadata/snap-3556048861821001954-0-de5280f4-97b4-4cb5-8394-b034ba16c73a.avro} (75%) delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-570726978145143711-0-49947273-2bcb-44c7-8273-f5084c3de7bb.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-6329113174899808939-0-17225286-c948-44c6-8594-1fb34271f9ba.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-7148257198741218111-0-0406ab25-5c95-4703-b985-e2a8252009d8.avro diff --git a/examples/08-idsse-warehouse.ipynb b/examples/08-idsse-warehouse.ipynb index dce2cfc..f995710 100644 --- a/examples/08-idsse-warehouse.ipynb +++ b/examples/08-idsse-warehouse.ipynb @@ -138,15 +138,15 @@ "text": [ "\n", "Processing match DFL-MAT-J03WMX...\n", - " ✅ Parsed in 1.46s, Saved in 4.83s\n", + " ✅ Parsed in 1.37s, Saved in 4.53s\n", " ✅ Rows: 145,967\n", "\n", "Processing match DFL-MAT-J03WN1...\n", - " ✅ Parsed in 1.34s, Saved in 4.39s\n", + " ✅ Parsed in 1.28s, Saved in 3.89s\n", " ✅ Rows: 141,561\n", "\n", "Processing match DFL-MAT-J03WOH...\n", - " ✅ Parsed in 1.29s, Saved in 3.86s\n", + " ✅ Parsed in 1.21s, Saved in 3.93s\n", " ✅ Rows: 137,214\n" ] } @@ -197,30 +197,12 @@ "name": "stdout", "output_type": "stream", "text": [ - "Warehouse Summary:\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ + "Warehouse Summary:\n", + " - Match DFL-MAT-J03WMX: 165,259 rows\n", + " - Match DFL-MAT-J03WN1: 162,273 rows\n", + " - Match DFL-MAT-J03WOH: 157,790 rows\n", "\n", - "thread '' (21731647) panicked at crates/tdc-core/src/datacontainer/core.rs:661:38:\n", - "called `Result::unwrap()` on an `Err` value: VariantNotFound\n", - "note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace\n" - ] - }, - { - "ename": "PanicException", - "evalue": "called `Result::unwrap()` on an `Err` value: VariantNotFound", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mPanicException\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[5]\u001b[39m\u001b[32m, line 9\u001b[39m\n\u001b[32m 6\u001b[39m lazy_container = catalog.load_container(match_id=m_id)\n\u001b[32m 8\u001b[39m \u001b[38;5;66;03m# Execute plan and pivot to wide format\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m9\u001b[39m loaded = \u001b[43mlazy_container\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcollect\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 11\u001b[39m rows = loaded.num_rows()\n\u001b[32m 12\u001b[39m total_rows += rows\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/python/tracking_data_container/catalog.py:653\u001b[39m, in \u001b[36mCatalogLazyTrackingDataContainer.collect\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 648\u001b[39m arrow_table = scan.to_arrow()\n\u001b[32m 650\u001b[39m \u001b[38;5;66;03m# No more redundant renaming here! Rust pivot handles snake_case now.\u001b[39;00m\n\u001b[32m 651\u001b[39m \n\u001b[32m 652\u001b[39m \u001b[38;5;66;03m# 4. Pivot to wide format using high-performance Rust implementation\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m653\u001b[39m wide_container = \u001b[43mTrackingDataContainer\u001b[49m\u001b[43m.\u001b[49m\u001b[43mpivot\u001b[49m\u001b[43m(\u001b[49m\u001b[43marrow_table\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 654\u001b[39m wide_container._catalog = \u001b[38;5;28mself\u001b[39m._catalog\n\u001b[32m 656\u001b[39m \u001b[38;5;66;03m# Apply final selection/slice\u001b[39;00m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/python/tracking_data_container/container.py:376\u001b[39m, in \u001b[36mTrackingDataContainer.pivot\u001b[39m\u001b[34m(cls, df)\u001b[39m\n\u001b[32m 373\u001b[39m df = pa.RecordBatch.from_arrays(df.flatten(), names=[f.name \u001b[38;5;28;01mfor\u001b[39;00m f \u001b[38;5;129;01min\u001b[39;00m df.type])\n\u001b[32m 375\u001b[39m \u001b[38;5;66;03m# Use the underlying Rust implementation for performance\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m376\u001b[39m inner = \u001b[43m_TrackingDataContainer\u001b[49m\u001b[43m.\u001b[49m\u001b[43mpivot\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdf\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 377\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m TrackingDataContainer(inner, validate=\u001b[38;5;28;01mFalse\u001b[39;00m)\n", - "\u001b[31mPanicException\u001b[39m: called `Result::unwrap()` on an `Err` value: VariantNotFound" + "Total rows in warehouse: 485,322\n" ] } ], @@ -252,9 +234,62 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading data for DFL-MAT-J03WMX...\n", + " ✅ Loaded and pivoted 165259 rows in 1.8728s\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "shape: (5, 424)
competition/idseason/idmatch/idframe_idperiodtimestampteams/away/players/DFL-OBJ-J01APO/player/idteams/away/players/DFL-OBJ-J01APO/team/idteams/away/players/DFL-OBJ-J01APO/xteams/away/players/DFL-OBJ-J01APO/yteams/away/players/DFL-OBJ-J01APO/zteams/away/players/DFL-OBJ-J01APO/velteams/away/players/DFL-OBJ-J01APO/accteams/away/players/DFL-OBJ-J01APO/entity_keyteams/away/players/DFL-OBJ-J01APO/nameteams/away/players/DFL-OBJ-J01APO/idteams/away/players/DFL-OBJ-J01APO/jersey_colourteams/home/players/DFL-OBJ-002G89/player/idteams/home/players/DFL-OBJ-002G89/team/idteams/home/players/DFL-OBJ-002G89/xteams/home/players/DFL-OBJ-002G89/yteams/home/players/DFL-OBJ-002G89/zteams/home/players/DFL-OBJ-002G89/velteams/home/players/DFL-OBJ-002G89/accteams/home/players/DFL-OBJ-002G89/entity_keyteams/home/players/DFL-OBJ-002G89/nameteams/home/players/DFL-OBJ-002G89/idteams/home/players/DFL-OBJ-002G89/jersey_colourteams/home/players/DFL-OBJ-00012X/player/idteams/home/players/DFL-OBJ-00012X/team/idteams/home/players/DFL-OBJ-00012X/xteams/home/players/DFL-OBJ-00012X/yteams/home/players/DFL-OBJ-00012X/zteams/home/players/DFL-OBJ-00012X/velteams/home/players/DFL-OBJ-00012X/accteams/home/players/DFL-OBJ-00012X/entity_keyteams/home/players/DFL-OBJ-00012X/nameteams/away/players/DFL-OBJ-0027G0/entity_keyteams/away/players/DFL-OBJ-0027G0/nameteams/away/players/DFL-OBJ-0027G0/idteams/away/players/DFL-OBJ-0027G0/jersey_colourteams/away/player/idteams/away/team/idteams/away/xteams/away/yteams/away/zteams/away/velteams/away/accteams/away/entity_keyteams/away/nameteams/away/idteams/away/jersey_colourteams/away/players/DFL-OBJ-0000RP/player/idteams/away/players/DFL-OBJ-0000RP/team/idteams/away/players/DFL-OBJ-0000RP/xteams/away/players/DFL-OBJ-0000RP/yteams/away/players/DFL-OBJ-0000RP/zteams/away/players/DFL-OBJ-0000RP/velteams/away/players/DFL-OBJ-0000RP/accteams/away/players/DFL-OBJ-0000RP/entity_keyteams/away/players/DFL-OBJ-0000RP/nameteams/away/players/DFL-OBJ-0000RP/idteams/away/players/DFL-OBJ-0000RP/jersey_colourteams/home/players/DFL-OBJ-000270/player/idteams/home/players/DFL-OBJ-000270/team/idteams/home/players/DFL-OBJ-000270/xteams/home/players/DFL-OBJ-000270/yteams/home/players/DFL-OBJ-000270/zteams/home/players/DFL-OBJ-000270/velteams/home/players/DFL-OBJ-000270/accteams/home/players/DFL-OBJ-000270/entity_keyteams/home/players/DFL-OBJ-000270/nameteams/home/players/DFL-OBJ-000270/idteams/home/players/DFL-OBJ-000270/jersey_colour
strstrstri64strdatetime[μs]strstrf32f32f32f64f64strstrstrstrstrstrf32f32f32f64f64strstrstrstrstrstrf32f32f32f64f64strstrstrstrstrstrstrstrf32f32f32f64f64strstrstrstrstrstrf32f32f32f64f64strstrstrstrstrstrf32f32f32f64f64strstrstrstr
nullnull"DFL-MAT-J03WMX"10000"first_half"2023-05-27 13:30:12.600"DFL-OBJ-J01APO""away"-13.825.719999null0.00.0"DFL-OBJ-J01APO|away"nullnullnullnullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-00012X""home"13.08-13.71null0.00.0"DFL-OBJ-00012X|home"null"DFL-OBJ-0027G0|away"nullnullnull"team:away""away"nullnullnullnullnull"team:away|away""FC Bayern München""DFL-CLU-00000G""#25282A"nullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-000270""home"-2.12-7.09null0.00.0"DFL-OBJ-000270|home"nullnullnull
nullnull"DFL-MAT-J03WMX"10001"first_half"2023-05-27 13:30:12.640"DFL-OBJ-J01APO""away"-13.7825.74null2.591.23"DFL-OBJ-J01APO|away"nullnullnullnullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-00012X""home"13.06-13.75null3.491.4"DFL-OBJ-00012X|home"null"DFL-OBJ-0027G0|away"nullnullnull"team:away""away"nullnullnullnullnull"team:away|away""FC Bayern München""DFL-CLU-00000G""#25282A"nullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-000270""home"-2.28-6.99null17.452.6"DFL-OBJ-000270|home"nullnullnull
nullnull"DFL-MAT-J03WMX"10002"first_half"2023-05-27 13:30:12.680"DFL-OBJ-J01APO""away"-13.7525.76null2.761.08"DFL-OBJ-J01APO|away"nullnullnullnullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-00012X""home"13.05-13.78null3.731.34"DFL-OBJ-00012X|home"null"DFL-OBJ-0027G0|away"nullnullnull"team:away""away"nullnullnullnullnull"team:away|away""FC Bayern München""DFL-CLU-00000G""#25282A"nullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-000270""home"-2.44-6.88null17.822.52"DFL-OBJ-000270|home"nullnullnull
nullnull"DFL-MAT-J03WMX"10003"first_half"2023-05-27 13:30:12.720"DFL-OBJ-J01APO""away"-13.7325.780001null2.890.91"DFL-OBJ-J01APO|away"nullnullnullnullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-00012X""home"13.03-13.82null3.951.24"DFL-OBJ-00012X|home"null"DFL-OBJ-0027G0|away"nullnullnull"team:away""away"nullnullnullnullnull"team:away|away""FC Bayern München""DFL-CLU-00000G""#25282A"nullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-000270""home"-2.61-6.78null18.192.44"DFL-OBJ-000270|home"nullnullnull
nullnull"DFL-MAT-J03WMX"10004"first_half"2023-05-27 13:30:12.760"DFL-OBJ-J01APO""away"-13.7125.809999null3.00.72"DFL-OBJ-J01APO|away"nullnullnullnullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-00012X""home"13.02-13.87null4.131.11"DFL-OBJ-00012X|home"null"DFL-OBJ-0027G0|away"nullnullnull"team:away""away"nullnullnullnullnull"team:away|away""FC Bayern München""DFL-CLU-00000G""#25282A"nullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-000270""home"-2.78-6.66null18.542.35"DFL-OBJ-000270|home"nullnullnull
" + ], + "text/plain": [ + "shape: (5, 424)\n", + "┌───────────┬───────────┬───────────┬──────────┬───┬───────────┬───────────┬───────────┬───────────┐\n", + "│ competiti ┆ season/id ┆ match/id ┆ frame_id ┆ … ┆ teams/hom ┆ teams/hom ┆ teams/hom ┆ teams/hom │\n", + "│ on/id ┆ --- ┆ --- ┆ --- ┆ ┆ e/players ┆ e/players ┆ e/players ┆ e/players │\n", + "│ --- ┆ str ┆ str ┆ i64 ┆ ┆ /DFL-OBJ- ┆ /DFL-OBJ- ┆ /DFL-OBJ- ┆ /DFL-OBJ- │\n", + "│ str ┆ ┆ ┆ ┆ ┆ 000… ┆ 000… ┆ 000… ┆ 000… │\n", + "│ ┆ ┆ ┆ ┆ ┆ --- ┆ --- ┆ --- ┆ --- │\n", + "│ ┆ ┆ ┆ ┆ ┆ str ┆ str ┆ str ┆ str │\n", + "╞═══════════╪═══════════╪═══════════╪══════════╪═══╪═══════════╪═══════════╪═══════════╪═══════════╡\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10000 ┆ … ┆ DFL-OBJ-0 ┆ null ┆ null ┆ null │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ 00270|hom ┆ ┆ ┆ │\n", + "│ ┆ ┆ ┆ ┆ ┆ e ┆ ┆ ┆ │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10001 ┆ … ┆ DFL-OBJ-0 ┆ null ┆ null ┆ null │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ 00270|hom ┆ ┆ ┆ │\n", + "│ ┆ ┆ ┆ ┆ ┆ e ┆ ┆ ┆ │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10002 ┆ … ┆ DFL-OBJ-0 ┆ null ┆ null ┆ null │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ 00270|hom ┆ ┆ ┆ │\n", + "│ ┆ ┆ ┆ ┆ ┆ e ┆ ┆ ┆ │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10003 ┆ … ┆ DFL-OBJ-0 ┆ null ┆ null ┆ null │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ 00270|hom ┆ ┆ ┆ │\n", + "│ ┆ ┆ ┆ ┆ ┆ e ┆ ┆ ┆ │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10004 ┆ … ┆ DFL-OBJ-0 ┆ null ┆ null ┆ null │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ 00270|hom ┆ ┆ ┆ │\n", + "│ ┆ ┆ ┆ ┆ ┆ e ┆ ┆ ┆ │\n", + "└───────────┴───────────┴───────────┴──────────┴───┴───────────┴───────────┴───────────┴───────────┘" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "target_match = pairs[0]['match_id']\n", "\n", @@ -271,9 +306,82 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "{'_entities': [{'entity_id': 'DFL-OBJ-J01N65', 'team_id': 'away'},\n", + " {'entity_id': 'DFL-OBJ-002G4A', 'team_id': 'home'},\n", + " {'entity_id': 'DFL-OBJ-J01D1W', 'team_id': 'home'},\n", + " {'entity_id': 'referee:DFL-OBJ-000141', 'team_id': None},\n", + " {'entity_id': 'DFL-OBJ-002G89', 'team_id': 'home'},\n", + " {'entity_id': 'DFL-OBJ-0002BO', 'team_id': 'home'},\n", + " {'entity_id': 'DFL-OBJ-0002F5', 'team_id': 'away'},\n", + " {'entity_id': 'DFL-OBJ-0027B6', 'team_id': 'home'},\n", + " {'entity_id': 'DFL-OBJ-0002AU', 'team_id': 'away'},\n", + " {'entity_id': 'DFL-OBJ-0026ZI', 'team_id': 'home'},\n", + " {'entity_id': 'DFL-OBJ-000191', 'team_id': 'away'},\n", + " {'entity_id': 'DFL-OBJ-0000RP', 'team_id': 'away'},\n", + " {'entity_id': 'DFL-OBJ-0002HE', 'team_id': 'home'},\n", + " {'entity_id': 'DFL-OBJ-0027KL', 'team_id': 'away'},\n", + " {'entity_id': 'referee:DFL-OBJ-00011W', 'team_id': None},\n", + " {'entity_id': 'DFL-OBJ-0000IA', 'team_id': 'away'},\n", + " {'entity_id': 'DFL-OBJ-J01B8N', 'team_id': 'away'},\n", + " {'entity_id': 'DFL-OBJ-J015S4', 'team_id': 'away'},\n", + " {'entity_id': 'DFL-OBJ-00012X', 'team_id': 'home'},\n", + " {'entity_id': 'DFL-OBJ-0027AX', 'team_id': 'home'},\n", + " {'entity_id': 'DFL-OBJ-0027G6', 'team_id': 'away'},\n", + " {'entity_id': 'DFL-OBJ-J017RE', 'team_id': 'away'},\n", + " {'entity_id': 'team:away', 'team_id': 'away'},\n", + " {'entity_id': 'DFL-OBJ-002GCR', 'team_id': 'away'},\n", + " {'entity_id': 'DFL-OBJ-000270', 'team_id': 'home'},\n", + " {'entity_id': 'referee:DFL-OBJ-00011J', 'team_id': None},\n", + " {'entity_id': 'DFL-OBJ-0028BD', 'team_id': 'home'},\n", + " {'entity_id': 'DFL-OBJ-0027V2', 'team_id': 'home'},\n", + " {'entity_id': 'DFL-OBJ-0026PM', 'team_id': 'away'},\n", + " {'entity_id': 'ball', 'team_id': None},\n", + " {'entity_id': 'DFL-OBJ-002GBW', 'team_id': 'home'},\n", + " {'entity_id': 'team:home', 'team_id': 'home'},\n", + " {'entity_id': 'DFL-OBJ-0002DR', 'team_id': 'away'},\n", + " {'entity_id': 'DFL-OBJ-J01APO', 'team_id': 'away'},\n", + " {'entity_id': 'DFL-OBJ-002GKO', 'team_id': 'home'},\n", + " {'entity_id': 'DFL-OBJ-J01BGM', 'team_id': 'home'},\n", + " {'entity_id': 'DFL-OBJ-J01DVC', 'team_id': 'home'},\n", + " {'entity_id': 'DFL-OBJ-0027G0', 'team_id': 'away'}]}" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "metadata = catalog.get_metadata(match_id=target_match)\n", + "metadata" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "ename": "FileNotFoundError", + "evalue": "No such file or directory (os error 2): ...rehouse/tracking/tracking_data/data/00000-0-2ce0e3ca-73c0-404e-9596-10570ff4a509.parquet (set POLARS_VERBOSE=1 to see full path)\n\nThis error occurred with the following context stack:\n\t[1] 'parquet scan'\n\t[2] 'sink'\n", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mFileNotFoundError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[7]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m start = time.perf_counter()\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m df = \u001b[43mpl\u001b[49m\u001b[43m.\u001b[49m\u001b[43mscan_parquet\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43midsse_warehouse/tracking/tracking_data/data/00000-0-2ce0e3ca-73c0-404e-9596-10570ff4a509.parquet\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcollect\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 4\u001b[39m query_time = time.perf_counter() - start\n\u001b[32m 5\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33m ✅ Loaded and pivoted \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mlen\u001b[39m(df)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m rows in \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mquery_time\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m.4f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33ms\u001b[39m\u001b[33m\"\u001b[39m)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/.venv/lib/python3.11/site-packages/polars/_utils/deprecation.py:97\u001b[39m, in \u001b[36mdeprecate_streaming_parameter..decorate..wrapper\u001b[39m\u001b[34m(*args, **kwargs)\u001b[39m\n\u001b[32m 93\u001b[39m kwargs[\u001b[33m\"\u001b[39m\u001b[33mengine\u001b[39m\u001b[33m\"\u001b[39m] = \u001b[33m\"\u001b[39m\u001b[33min-memory\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 95\u001b[39m \u001b[38;5;28;01mdel\u001b[39;00m kwargs[\u001b[33m\"\u001b[39m\u001b[33mstreaming\u001b[39m\u001b[33m\"\u001b[39m]\n\u001b[32m---> \u001b[39m\u001b[32m97\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunction\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/.venv/lib/python3.11/site-packages/polars/lazyframe/opt_flags.py:328\u001b[39m, in \u001b[36mforward_old_opt_flags..decorate..wrapper\u001b[39m\u001b[34m(*args, **kwargs)\u001b[39m\n\u001b[32m 325\u001b[39m optflags = cb(optflags, kwargs.pop(key)) \u001b[38;5;66;03m# type: ignore[no-untyped-call,unused-ignore]\u001b[39;00m\n\u001b[32m 327\u001b[39m kwargs[\u001b[33m\"\u001b[39m\u001b[33moptimizations\u001b[39m\u001b[33m\"\u001b[39m] = optflags\n\u001b[32m--> \u001b[39m\u001b[32m328\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunction\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/.venv/lib/python3.11/site-packages/polars/lazyframe/frame.py:2429\u001b[39m, in \u001b[36mLazyFrame.collect\u001b[39m\u001b[34m(self, type_coercion, predicate_pushdown, projection_pushdown, simplify_expression, slice_pushdown, comm_subplan_elim, comm_subexpr_elim, cluster_with_columns, collapse_joins, no_optimization, engine, background, optimizations, **_kwargs)\u001b[39m\n\u001b[32m 2427\u001b[39m \u001b[38;5;66;03m# Only for testing purposes\u001b[39;00m\n\u001b[32m 2428\u001b[39m callback = _kwargs.get(\u001b[33m\"\u001b[39m\u001b[33mpost_opt_callback\u001b[39m\u001b[33m\"\u001b[39m, callback)\n\u001b[32m-> \u001b[39m\u001b[32m2429\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m wrap_df(\u001b[43mldf\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcollect\u001b[49m\u001b[43m(\u001b[49m\u001b[43mengine\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcallback\u001b[49m\u001b[43m)\u001b[49m)\n", + "\u001b[31mFileNotFoundError\u001b[39m: No such file or directory (os error 2): ...rehouse/tracking/tracking_data/data/00000-0-2ce0e3ca-73c0-404e-9596-10570ff4a509.parquet (set POLARS_VERBOSE=1 to see full path)\n\nThis error occurred with the following context stack:\n\t[1] 'parquet scan'\n\t[2] 'sink'\n" + ] + } + ], "source": [ "start = time.perf_counter()\n", "\n", diff --git a/examples/idsse_warehouse/catalog.db b/examples/idsse_warehouse/catalog.db index 346745a2df576e812f2668a870b2c4e72dcd571a..5dd3c79decc78f46e971b2ab6380ffcc5e3468e4 100644 GIT binary patch delta 178 zcmWN~y$ypv3_xMAKsEpxT1#i+yR$dQ8utBkX;V|DrAx&KNEs%3WP;E?eQ!I4?HJDI zxN!C))d`Sl2QE2+1WsTnqY}n$YWsV9Eohy%rvg{Uz$p;8Awo}{$#a&bzTcw3h6T-|ydmHJrEI(Na^>rlVp6B!*!RCLsOk?ytlBb$H)S zbC76;CiqYRE-QldPLR>PRHGi%x990G>l&p|A~+!dcQokE0z{U@v89-Am-BaKYt$qw x$jSvcnu7L$z}_<@7>nyZUbpvjo)Zlvb{xE{z||H|t^U5NxQauy3l_wURuvKEhQnr;`o$l zGbd*)UB6yFYED$hn)rYz^QTOSnlfunV9=WPlcJ{0njAkNYDs+1lo@m8&YUhce^SIG ziTQKphfkU@lU>Art;ZSN6P~))&iiQU`St4i>v)|#p|9)LntkECuTH(^xsLO`x}GeV zSJ{Qx6c%JJ$exmvmoR14tQE7<(q=BpPMVUnCOb1}%98B7gsdr96H@Z#%t=}nJ|Q9` zVph~4@dpwj60f8VeMwVO7G}T9>o$#R8&{vO9*+Z`Ft@Pu1SUht13qkQ3hk|)$Bv$L zzkc1&<@)IZ7I(~>oEJuE+L^A`P}!_vyfxyam*TEj60W8WhaJ?67#Mh&V$s6|0CQ`H ArT_o{ delta 301 zcmbQQJV$v$r9i#XgoR}~#cLmY(9k-qZq66R)WFi~W9`RmDDow0b%wT|gWyv0dmH9= zm}mqoaA;sqU9@0B&U^0cjrKY0VO;JGr&u)v?=X5*y)wDOn$MLLXPBR6Vl1)Wrhf0< z_4ao4HZ2#-<5n;on{sIc-FX^`2oj5_IP(v(J#goZKb)%25(j*I| zA`!mtPa7K|`o2l6dp|v)Tu&{0`sc-x_tx0TJm`JCeXm)Ct&3lR{P#PrHEsp9%=z%~ w-IpIz=J=*PQR!KCzTL6jG(j~WGF5cO!5#IRfBC;S*<0HELVU;aX!Jk<0Kc({EdT%j diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/e501b09a-d22e-4e7e-9606-c1ea5899be89-m0.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/62719280-2ab0-4ae1-ba75-d57e508139d6-m0.avro similarity index 93% rename from examples/idsse_warehouse/tracking/match_metadata/metadata/e501b09a-d22e-4e7e-9606-c1ea5899be89-m0.avro rename to examples/idsse_warehouse/tracking/match_metadata/metadata/62719280-2ab0-4ae1-ba75-d57e508139d6-m0.avro index 965f944d2ac5d23a6b2619e0529be381d9e8eeb2..3d73cb8cfc3d57e53e1f0f75768fd6d97a42e86c 100644 GIT binary patch delta 145 zcmbQIJWqK;rGUchpruT&ydLalo=@tvM0^RS}}V{M&7E~Nm=uupV-;_l15a$ozqjkAO`a)G;5s`i zD`7@T!ps%3lTxOv%AS-pZ&h|?TEeo~IWuNuWF=e`06zOEzV1)@at_V%C%W1QnRxADmnuC@uViAqL%I0Q96lSO5S3 diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/63524640-981e-48a3-b912-da48df7f23e7-m0.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/8c35a04a-3f37-4fe4-9d28-d33b25ff80bf-m0.avro similarity index 92% rename from examples/idsse_warehouse/tracking/match_metadata/metadata/63524640-981e-48a3-b912-da48df7f23e7-m0.avro rename to examples/idsse_warehouse/tracking/match_metadata/metadata/8c35a04a-3f37-4fe4-9d28-d33b25ff80bf-m0.avro index 3d48e49b8ce40c865afe447e1f1774ff2f5d9b5c..1cb4ba8923560655e55056f675b9a752d3e73cfc 100644 GIT binary patch delta 304 zcmbQMJYRW3r9gevdHaF`EbiM`&+O2$SUl+vQyWWi(k;{XuWyMj@7KG1>coKq1$~;j z#_CtS;^XbNSZrMpv44Svy{6_?4O7d-OCs*8Zq+eW->p`f~N0!g^epV zm~3s)KVYa~aK*GGGge+%nEf)Z+cd6iTz$@EJPv%q+``fmm<%Nk_^`1lw6}U5J9^gr z`gKE>>!%M`+_~TEyf8}B&UC$o%4QYgtq~`^6nD*%a5Z%}?4V}Ez%cc140^Z#08*)e Aa{vGU delta 302 zcmV+}0nz@SBb_6#c@Tf}%zyM~#6iV;`32@peMYarQFpX(VI7|t=J zW#*(>>Feu<7N-^!>lYN|Cncp8r6y;T6zjXBmZj$87o-;HmlP!?XJ_W6$EPHgB*rJ_ z=anR8<^jc1D-v@Ha#D-+GgFF-Q{&4Mi&8W4ON&$C8uW7$OOk&x;&W3=5Y7+28tW!m8XD=QB$`;Hq?xA~8K;`-6(klFmZp~Q1vmx; zhPs9@y_w0##KClfk)?vIg5!h>1CYbS!o&uoKuiWOlYs%qVPTSSar4pjbqvwh8TlCC-^k~FE#eDe%=1qM@yo4o? A{{R30 diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-7979098971138712334-0-540bf63c-5ea6-4c6d-b543-c7012c9be4c7.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-6306903004315971787-0-62719280-2ab0-4ae1-ba75-d57e508139d6.avro similarity index 77% rename from examples/idsse_warehouse/tracking/match_metadata/metadata/snap-7979098971138712334-0-540bf63c-5ea6-4c6d-b543-c7012c9be4c7.avro rename to examples/idsse_warehouse/tracking/match_metadata/metadata/snap-6306903004315971787-0-62719280-2ab0-4ae1-ba75-d57e508139d6.avro index ec3e94088bd5854639f3f533364b03cee3617438..4ee36360ecaef74b59b048a05f0c8e7569a9a85e 100644 GIT binary patch delta 346 zcmaFM_lnQQKPiimN31w6v7k63zeG1PMa|6Ez|7LX*ucQV*wEC{+|b;@T%{nfC^fG{ z7g?Ewk%6JPk*TSXk+G4P8BnpM@kSpRw)%f3f-iJ2PjS0YG>zlS{i#P-_AwtlXX|&^ zK!){$z*nndeV-q;?R>;`=HmTt+?})ZZ{|H@$ysx!k<(1<3gZ$Zr7Bjv#u*w;S^W)P z+Lo)^zTDpXdfQ>i=YI|zuC&`3m~AmbtzzE+sk;^jKR!62bNk_@O(H9d9t*6H{bs#x z<$SA(d-YN817jyT8?UqBW4{+{;Cog1!uzT6UW%bXr*M5Q3HC^fG{ z7g?E!nSq6Yv4xSTnX##nxrMQrf#pUY8MgXIA`wip_A)aqIQLy7#_i=(mLtqZ&l&n1 zHjrR_An?`dnCBY{PUA;xa~I#=|9`>?Pnny`N|KWn1%m}|e83QXw<1=WBVe&CbIIe( z<-9z>{`K?9k5#YAKNy&uZ*!L2&f&l|IgJDFr=C}x-kCSWVVbapTA<{zVoeD#&sm8} zJ(CW$J(0O~ZeN`1#iWm0YVX^>=gXg;UMTYS>5up-)lWWJGdHaA={zaKayfLCA@e2^ k--{_tX@4Is>3_U_dVBxh$A7=y?fK7`719FtH9OSX05gb~iU0rr diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4709260392851115659-0-1c479d52-bc34-4f1d-a57d-7f1624b94057.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-6532061344649095069-0-8c35a04a-3f37-4fe4-9d28-d33b25ff80bf.avro similarity index 73% rename from examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4709260392851115659-0-1c479d52-bc34-4f1d-a57d-7f1624b94057.avro rename to examples/idsse_warehouse/tracking/match_metadata/metadata/snap-6532061344649095069-0-8c35a04a-3f37-4fe4-9d28-d33b25ff80bf.avro index 8d3884c2e11a9cfbf8daf019a16ee0c5d60ef28a..95ee30e8ad133bbd746c37b4ff3af96dbb2c666a 100644 GIT binary patch delta 398 zcmcb}KabzXKPiimN31w6v7k63zeG1PMa|6A*vP=l(AdPp%*4{b($v7rQl%iVC^fG{ z7e$#dP{P32z`(@V(A3i0(A>g&qmK+*{fr-%d4F#5P*)RpDaQZ0MU8D4%h?O|Ud)an zY#&NFcb+X)Gu~~z``RPkHPg+l^UoxG<2TO!!cw+v+O($D4CN*duA@sf2o$qQ>nn6j z(rk|zLr{hGSKji&Fy7|y|bptyp|FBow|3r^cw^H zS-a{FZ;Ja;(h_Q|bl&T+?bG%b88>7qqIe@(V-3$}zbddyT|Gg}$W2bsLtE5kn$n3L zkAog7U2}3zZEfw!e{{oi@?3=%-`}6J)4M-^{<(a!Lkd53zKnk*@Q$1$7@kRDLS8t&e`apYi(c6X@Xt01uI|Z~y=R delta 449 zcmbQof05tEKPiimN31w6v7k63zeG1PMa{(Az|zRfz}V8r!qm{v(A3P-Ql%iVC^fG{ z7g?FPp^1f&skx!0g}I5Lk)Z`p=|&$Jw)#BV@LO}6zv~_@W!Ko&cWDz_59`tMw%*K+ z0u2vM+qV2;{C{Xyb@lF>*4Bz09T(j$>b*HW$I_?h;k%w|x0Y@4n0o5eDYqOQ7R8PX zw&wM63fwjDXcUiqQoxkh6yK!X8t@X<{bEFz& z7<_JhY8n%B>&WUWcke|==C@b{8wL4&_A+j4{mA-k(yr`?-}4R}m~xzD@s;iOPTbhW zk-GW*+4k*|A5ZVTz3}7f#nv}>yzBY?rRUD~ohg@&`bhU$nhRAQ=3)7|qfpmlt^2Wk jl11yzN1IRo-oJMzFemddde8y@WRBGu diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-4608038256352783609-0-63524640-981e-48a3-b912-da48df7f23e7.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-8201725522326604393-0-2d702eea-f69f-42e3-942b-7052e0b73cd6.avro similarity index 83% rename from examples/idsse_warehouse/tracking/match_metadata/metadata/snap-4608038256352783609-0-63524640-981e-48a3-b912-da48df7f23e7.avro rename to examples/idsse_warehouse/tracking/match_metadata/metadata/snap-8201725522326604393-0-2d702eea-f69f-42e3-942b-7052e0b73cd6.avro index 0de28193245ccebbecef162ed448f14b7c0ff9cd..9cd86678dad2fe4c661316c2103f1d88af4e262d 100644 GIT binary patch delta 249 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 a185fj9?RBMuO?@DpC9@CrBBpsAp?tVDLx$l diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/0406ab25-5c95-4703-b985-e2a8252009d8-m0.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/1b21427f-2ee9-4e4c-9e70-94eb1b10fbd5-m0.avro similarity index 75% rename from examples/idsse_warehouse/tracking/tracking_data/metadata/0406ab25-5c95-4703-b985-e2a8252009d8-m0.avro rename to examples/idsse_warehouse/tracking/tracking_data/metadata/1b21427f-2ee9-4e4c-9e70-94eb1b10fbd5-m0.avro index 32cfd245cff01fe53746ff2d39cbe51d632aab45..9bebc1d82af9f99d27b2f7b0392e9629244615d9 100644 GIT binary patch delta 1306 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 diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/49947273-2bcb-44c7-8273-f5084c3de7bb-m0.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/de5280f4-97b4-4cb5-8394-b034ba16c73a-m0.avro similarity index 76% rename from examples/idsse_warehouse/tracking/tracking_data/metadata/49947273-2bcb-44c7-8273-f5084c3de7bb-m0.avro rename to examples/idsse_warehouse/tracking/tracking_data/metadata/de5280f4-97b4-4cb5-8394-b034ba16c73a-m0.avro index 48502db585abb55f1dcd35752253f46d4a865a60..7b9807753306d0316f04e39818808a3fbdbbd2d6 100644 GIT binary patch delta 1258 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 aPcsX98^;!^OP_!HTlOVu%Y)|C_51^`eq>bu literal 0 HcmV?d00001 diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-5888034176243899453-0-e501b09a-d22e-4e7e-9606-c1ea5899be89.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-3556048861821001954-0-de5280f4-97b4-4cb5-8394-b034ba16c73a.avro similarity index 75% rename from examples/idsse_warehouse/tracking/match_metadata/metadata/snap-5888034176243899453-0-e501b09a-d22e-4e7e-9606-c1ea5899be89.avro rename to examples/idsse_warehouse/tracking/tracking_data/metadata/snap-3556048861821001954-0-de5280f4-97b4-4cb5-8394-b034ba16c73a.avro index 71d347e2c806960b716749091844c6efb204df98..9473c53ca6b427be62fed262b03b3ec1add7be5e 100644 GIT binary patch delta 402 zcmbQkKY>5aKPiimN31w6v7k63zeG1PMa|gM)Xc!d!otkZ!pP9Tz|hjvM5Q3HC^fG{ z7g?F1g{85jv5|qXnSrs9iG`7=sfAo|YGG+=UUI5#UTJPpY7vvs#yBpv`kb$p#rqV0 zc$5nIt?s%TKbNJ8<>>jnUPl~6SRW`pGpuQSeMUui^G0DY?!vVqZ>Q~?ENwo+KJ8J~ zd+Q%UVq)LI7{enrY}j=1+$Ex$%TmrW^Z zYact$3JNq&sm$JLd1iHU;i_m`v(l^eulBsEwz`-aJmGQWE1BN@txG*?pS(!_P<_%n z>+AK&K_WaZ%M>~T8;v9mD)@*j;1JE)ne`~ZSu^a*m23QMhWFq5=|6k!zy13Q<8R+; zwjY`CeB-}q?b6W(QTH2t1>fbKK6)nNyT40kV~FD%M~5j5N~sD9BODfSsCrFIl&Te8 le@V;#-0!D1zdk-&v#jdTo3BsvA7#`V8h>M)mOT?aWB}58x2ymF delta 414 zcmbQhKZifgKPiimN31w6v7k63zeG1PMa|U0!otAV#L(Q#$i&#f($d7#SfwDbC^fG{ z7g?FPrMabnrG=%rp`o#bxuKDqM{Tm_v8X$Zr8|sw8Xo*(C_KN+EK2i6 z)D6z_tQ<=I2c1sv>8!oekf9vrb|lB~p;$XJyWP77&r@5ar(DWyzjpE5wy4Ff!C(6F z9x<)n=ImX~owhxv>=I9vhwtq#0cQ2d{p^Bdq zy_Po!a+?G@7@eEQ(KAuwq>3S<(-e)RhJj1cUT@g&_V2XkXP?hs?|%67@xy1I&U#;I zZ~kYhosiFSBNG*;2`&;d1!X%Y%$UJ?r>7$EQT*SxE9Rfo^OK(*p1ykb^ZEtT6OXit x^q8$sk*J^V;IW0v;-rI0q}+3xBcA)EzV7>9R{Lp-jeVW{oDcD{jgF&-5&)a1vu6MR diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-570726978145143711-0-49947273-2bcb-44c7-8273-f5084c3de7bb.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-570726978145143711-0-49947273-2bcb-44c7-8273-f5084c3de7bb.avro deleted file mode 100644 index e0af09b89014305619eb12bbb208e3a497aee1fd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1849 zcmb7EPe>F|9JVz~NFqYkHA9y{3&GVLbzQB95Rx`T%ZhA7hPN|s-O-tO+ZU-mkKP+Yjp`CYJ;LvT{{GUc<2@gLA}}CSw}}lyT14PzVG+@et(98lLu;? znhX>zjIc-Gn|*$-zq6;$-%EG<=MrO*D%R%SK|_S{#11O7Nkv3NV+)#?gauuP)fC9FBr~HFBg;+`UXbxt zIEQ3ROx_Nn{dORu0c^1W7O>9*wq`5|B-c_4cVpB}5*4c$$RGS0Njev%lWYBNp5@Lu{y5IOO(fIvlTTJxo0u$fZJE8C z$R!d?B9px`vivK~Bs25uL*_9%l1ODAr574=@zJ;|m(AUXr_zUNhgU9s^5g@aZ(jym z+lL->47XkSobT`q-J3f9bt$*V#H&N8Q6`gWNH?q>Z)rKTmPs`)t}}OE3Qwz?CqKS# zUHTaayna=6`#!v|k&D;6QmNl7i!;jl(&5#On=aSTosD-j$GD3A`Z|BpIQ!w~ct_
cSfX2&T2!2wpT}e* zl2}%hua}&klA6qylA4y2SduCR7Aj88NX<=LCtj^oQdy9yWTl`~l$xAhl%k}gpp=%G znv+rt6pOA_%1g`z%jPELWu~PTm&6w&mSn&c0o@N&W@=yn)CDuAxTGjEFCC;LB|jNt zuupz+VhPYS3ZX%s3gww4847N}3Sj$Fm1=bqu-K84nwO5n7DFUka`J(8py&pMSxF|y zp7`Q|)Z}=e%V1uCxzY$pe`a0@s`ljkyb@rTqiZrW$EOJv0r8Lsz|d@pq#4u2xtTz> zLZc9u31)arNK8pdO^Jsk6KsAqCPIH&W=?8xd~$v%ruPj@ko04OS!zXQaS1T8w-PEx96V40*WI(^^SV`2wG7bHvG z`N`JCdi3H(FJ?ypwg;wdHm&Oo-o3pW_wJe>hf>m(z%AUntBao*oNV1|eCsz~SlG5E z%{4hWJQ9iu8eC>={s$xLdN^vdm%uZ8JO+PUWCmZ_(gq`u#wKKm_Onn~v-p6uP15^V3DEC~5=dx`Iv zr{@nbUH^J_15eZk0gLOY9JL1j@`6nuUoUH&)!sDXF~0N&T4u)6vH4r_L`T7Z=&AQ_+5X zKw46M?Z${VZ_C+_pTEw3O2|Gg|G)aW4IgbRm4fv)zwbKeDso_Awt$3%f`!(y8N#bi o{8ydlet*vS%gv`BUR`ircjNzm-|RQ;vYf~HlS$3-DSD^^07RIpa{vGU literal 0 HcmV?d00001 diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-7148257198741218111-0-0406ab25-5c95-4703-b985-e2a8252009d8.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-7148257198741218111-0-0406ab25-5c95-4703-b985-e2a8252009d8.avro deleted file mode 100644 index 80676bd297b11d6b9b15650366485cb736dc98e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1938 zcmb7ET}V@57&ejYVs#N^OUuSV2Z6JFYjd-1iolvkmKE77j-TgzbH~olI_Fz!*)F{3 zA}F*q<~EJpSYcQ~1cB5=KZJr6UGyJ8K||~!x-1I%&h*TqM`zuB-}^kz`#$eab4S+- zQ<);dpc0UN{-2hZ7 z=u`-a46=zKp##aqj!k|^79h5rMzX?5VzFZ#JS|JLjKm^l4U7Cd#HcLyq%Z*_z}p+T zDI5tRYKMiA5hKYgRb!$29O7ApytQ>xA{27J0u(tvQZOBaIFJt_>60**-A+&%kAh`R z>{LToiBXB0BnD!Vjpay-*AnJ%z|wHYlG8_2w8pZC29LzfMO1)Jg_nrNJT$QkbE*!l z1QA1%%(POpEFC&LC*t{VMiMcxd?5(93xTu-&@&A%fL%JUF=K%vxz4t5DTca&43K3J zOKVfISk2r20q^H{q|l5MGL~<5>hK27&>2p_q}g;K2E7Lc1{U!MlM)IM=q)f1%s3!R z;Ux+H`lcIOkguQsLtzWG3JMVDJv63}pIc?&_}F!`*G7pUo+lA=3kzi#MpO*(P*AVM z>@s@D6NizYZtz@m;q_LhA9A8X#xSQca|1NSdfgCoND_~rXk4BdIQb#LxeGFj25LUD z$01(@rO})qpX4yX5RXZ^s`|X8=A|%_=?*fQhIIzIqod=EV5CsXu!e%cC0TVx<{wy9 zA?-xUF1v?{Mwih(i6XRsFk~Ty2CH;N|EGBVKC7EVMT-Jo-v8dPntynC^jG-pu(P)Pn(7T=)n@Cbb%o{7hO@$}%qmXYrFIQ`!u-v8%$J+?|d!$2>% zW4E8{4yRH*Jr^#lScQ&%Jcpy3_uvPni;w@9Uj8(h_O2Z7wI&kQY(w_xSc7-MJ7%>` zWXH-ZVtTmey!Xzj#+#9rwUfP>*zQm7?uqXo-nvnHKU;eC$CLi%w-2T&dY*T-7Zr`% I?O$lkU(^7PD*ylh diff --git a/python/tests/test_catalog.py b/python/tests/test_catalog.py index b12ea1c..1eba3bb 100644 --- a/python/tests/test_catalog.py +++ b/python/tests/test_catalog.py @@ -7,6 +7,7 @@ def test_catalog_lifecycle(tmp_path): catalog.initialize() # Create a container with all required fields for Iceberg + # and some constant attributes (jersey_color, player_name) data = { "match/id": ["match_1", "match_1"], "timestamp": [1000000, 1040000], # microseconds @@ -14,17 +15,31 @@ def test_catalog_lifecycle(tmp_path): "period": ["1", "1"], "ball/x": [0.0, 1.0], "ball/y": [0.0, 1.0], + "teams/home/players/p1/x": [10.0, 11.0], + "teams/home/players/p1/y": [20.0, 21.0], + "teams/home/players/p1/jersey_color": ["red", "red"], + "teams/home/players/p1/name": ["Player 1", "Player 1"], } - # Note: player/id and team/id will be automatically added by melt() for the ball container = tdc.TrackingDataContainer(data, validate=False) - # Save container + # Save container - normalization should happen here catalog.save_container(container) + # Verify normalization: physical table should not have jersey_color + table = catalog._iceberg_catalog.load_table("tracking.tracking_data") + physical_cols = [f.name for f in table.schema().fields] + assert "jersey_color" not in physical_cols + assert "name" not in physical_cols + # Load container lazy_loaded = catalog.load_container(match_id="match_1") assert isinstance(lazy_loaded, tdc.LazyTrackingDataContainer) + # Schema should include normalized columns + schema_names = lazy_loaded.schema().names() + assert "teams/home/players/p1/jersey_color" in schema_names + assert "teams/home/players/p1/name" in schema_names + loaded = lazy_loaded.collect() assert loaded.num_rows() == 2 assert loaded.is_bound() @@ -32,10 +47,13 @@ def test_catalog_lifecycle(tmp_path): # Check data df = loaded.to_polars() assert df["match/id"][0] == "match_1" - # player/id was an input, and stays as player/id in wide format assert "ball/player/id" in df.columns assert df["ball/x"][0] == 0.0 + # Check normalized constants + assert df["teams/home/players/p1/jersey_color"][0] == "red" + assert df["teams/home/players/p1/name"][0] == "Player 1" + def test_bound_container_save(tmp_path): warehouse_path = tmp_path / "warehouse" diff --git a/python/tracking_data_container/catalog.py b/python/tracking_data_container/catalog.py index 0ec61aa..e08f70f 100644 --- a/python/tracking_data_container/catalog.py +++ b/python/tracking_data_container/catalog.py @@ -237,7 +237,7 @@ def save_container( else: raise ValueError("match_id must be provided if not present in container data") - # 1. Save tracking data and get unique entities + # 1. Save tracking data and get unique entities (with constant attributes) entities = self._save_tracking_data(container, match_id) # 2. Prepare metadata @@ -290,12 +290,12 @@ def _save_tracking_data( self, container: TrackingDataContainer, match_id: str, - ) -> list[dict[str, str]]: + ) -> list[dict[str, Any]]: """ Save tracking data to Iceberg table in entity-long format. Returns: - List of unique entities (dict with entity_id, team_id) + List of unique entities (dict with entity_id, team_id, and constants) """ if self._tracking_table is None: self._tracking_table = self._iceberg_catalog.load_table("tracking.tracking_data") @@ -324,8 +324,51 @@ def _save_tracking_data( # Ensure match_id is present and correct long_df = long_df.with_columns(pl.lit(match_id).alias("match_id")) - # Extract unique entities before saving - entities = long_df.select(["entity_id", "team_id"]).unique().to_dicts() + # --- Constant Extraction --- + # Identify attributes that are constant for each entity + # These will be stored in metadata instead of the physical table + ignore_for_constants = { + "match_id", + "frame_id", + "period", + "timestamp", + "entity_type", + "entity_id", + "team_id", + "x", + "y", + "z", + "entity_key", + } + potential_constants = [c for c in long_df.columns if c not in ignore_for_constants] + + entities_with_constants = [] + if potential_constants: + # Group by entity and check uniqueness of potential constants + agg_exprs = [pl.col(c).n_unique().alias(f"{c}_count") for c in potential_constants] + agg_vals = [pl.col(c).first().alias(f"{c}_val") for c in potential_constants] + + stats = long_df.group_by(["entity_id", "team_id"]).agg(agg_exprs + agg_vals) + + # We'll drop columns from the physical table if they are constant for ALL entities + columns_to_drop = [] + for c in potential_constants: + if stats[f"{c}_count"].max() == 1: + columns_to_drop.append(c) + + # Prepare the entity list for metadata + # For each entity, we store the values of the dropped columns + entities_with_constants = ( + stats.select(["entity_id", "team_id"] + [f"{c}_val" for c in columns_to_drop]) + .rename({f"{c}_val": c for c in columns_to_drop}) + .to_dicts() + ) + + if columns_to_drop: + logger.info(f"Normalizing constant attributes to metadata: {columns_to_drop}") + long_df = long_df.drop(columns_to_drop) + else: + entities_with_constants = long_df.select(["entity_id", "team_id"]).unique().to_dicts() # 3. Validation of required fields required_fields = [ @@ -341,8 +384,8 @@ def _save_tracking_data( for field in required_fields: if field not in long_df.columns: raise ValueError(f"Required field {field} is missing from container") - # if long_df[field].null_count() > 0: - # raise ValueError(f"Required field {field} contains null values") + if long_df[field].null_count() > 0: + raise ValueError(f"Required field {field} contains null values") # 4. Convert to Arrow and handle types arrow_table = long_df.to_arrow() @@ -388,7 +431,7 @@ def _save_tracking_data( # Append long-format data txn.append(arrow_table) - return entities + return entities_with_constants def _save_metadata(self, metadata: dict, match_id: str) -> None: """Save metadata to Iceberg table.""" @@ -467,7 +510,7 @@ def schema(self) -> Fields: assert self._catalog._tracking_table is not None - # Load entities from metadata (Strictly no fallback scan) + # Load entities from metadata metadata = self._catalog.get_metadata(self._match_id) entities = metadata.get("_entities") @@ -488,7 +531,7 @@ def schema(self) -> Fields: "period_id", "entity_key", } - # Anything else is an attribute + # Physical attributes in the tracking table attr_cols = [c for c in physical_names if c not in ignored and c != "id"] # Base list: all core columns that are NOT entity-specific @@ -505,9 +548,16 @@ def schema(self) -> Fields: t_id = str(row.get("team_id", "none")) prefix = TrackedEntity.from_entity_key(f"{p_id}|{t_id}").cdf_prefix() + # 1. Attributes from physical table for attr in attr_cols: wide_names.append(f"{prefix}{attr}") - # Map back entity_id and team_id as attributes with standard CDF names + + # 2. Attributes from metadata constants + for attr in row.keys(): + if attr not in ("entity_id", "team_id"): + wide_names.append(f"{prefix}{attr}") + + # 3. Standard CDF names wide_names.append(f"{prefix}player/id") wide_names.append(f"{prefix}team/id") @@ -562,6 +612,9 @@ def collect(self) -> TrackingDataContainer: selected_long_fields = None deferred_select = None + metadata = self._catalog.get_metadata(self._match_id) + entities_metadata = metadata.get("_entities", []) + if self._selection: eager_schema = self.schema() literal_names: list[str] = [] @@ -647,12 +700,31 @@ def collect(self) -> TrackingDataContainer: scan = self._catalog._tracking_table.scan(**scan_kwargs) arrow_table = scan.to_arrow() - # No more redundant renaming here! Rust pivot handles snake_case now. - # 4. Pivot to wide format using high-performance Rust implementation wide_container = TrackingDataContainer.pivot(arrow_table) wide_container._catalog = self._catalog + # 5. Inject constant attributes from metadata + if entities_metadata: + # Reconstruct prefixes for entities in metadata + entity_map = { + f"{e['entity_id']}|{e.get('team_id', 'none')}": e for e in entities_metadata + } + + wide_names = wide_container.schema().names() + for ek, ent_data in entity_map.items(): + prefix = TrackedEntity.from_entity_key(ek).cdf_prefix() + for attr, value in ent_data.items(): + if attr in ("entity_id", "team_id"): + continue + + wide_attr_name = f"{prefix}{attr}" + if wide_attr_name not in wide_names: + # Add constant column + wide_container = wide_container.add_column( + wide_attr_name, [value] * wide_container.num_rows() + ) + # Apply final selection/slice if deferred_select: wide_container = wide_container.select(deferred_select) From 220c17304804f932e801fe61f9088beee655386f Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Wed, 8 Apr 2026 15:31:27 +0200 Subject: [PATCH 11/21] feat: allow null values in x, y, and z fields in catalog and pivot --- crates/tdc-core/src/datacontainer/core.rs | 8 - examples/08-idsse-warehouse.ipynb | 170 +++--------------- examples/idsse_warehouse/catalog.db | Bin 20480 -> 20480 bytes ...7d68-44e1-b7c9-ca6e892cf286.metadata.json} | 2 +- ...-f1ab-4b8b-adb2-38b3470d05ad.metadata.json | 1 - ...-a4f0-4ac9-9cce-852bb885ebb5.metadata.json | 1 - ...-4899-4293-a3d4-319f62f14c74.metadata.json | 1 - ...702eea-f69f-42e3-942b-7052e0b73cd6-m0.avro | Bin 4511 -> 0 bytes ...719280-2ab0-4ae1-ba75-d57e508139d6-m0.avro | Bin 4510 -> 0 bytes ...35a04a-3f37-4fe4-9d28-d33b25ff80bf-m0.avro | Bin 4511 -> 0 bytes ...-62719280-2ab0-4ae1-ba75-d57e508139d6.avro | Bin 1898 -> 0 bytes ...-8c35a04a-3f37-4fe4-9d28-d33b25ff80bf.avro | Bin 1950 -> 0 bytes ...-2d702eea-f69f-42e3-942b-7052e0b73cd6.avro | Bin 1826 -> 0 bytes ...f108-4a16-b20b-39ce0ac55542.metadata.json} | 2 +- ...-0fac-4795-b9f9-7b786ee6fc78.metadata.json | 1 - ...-2df4-4602-ba11-f6b72b778e9b.metadata.json | 1 - ...-12df-4c75-8046-a6719215fa9a.metadata.json | 1 - ...225286-c948-44c6-8594-1fb34271f9ba-m0.avro | Bin 6385 -> 0 bytes ...21427f-2ee9-4e4c-9e70-94eb1b10fbd5-m0.avro | Bin 6431 -> 0 bytes ...5280f4-97b4-4cb5-8394-b034ba16c73a-m0.avro | Bin 6383 -> 0 bytes ...-1b21427f-2ee9-4e4c-9e70-94eb1b10fbd5.avro | Bin 1849 -> 0 bytes ...-de5280f4-97b4-4cb5-8394-b034ba16c73a.avro | Bin 1936 -> 0 bytes ...-17225286-c948-44c6-8594-1fb34271f9ba.avro | Bin 1998 -> 0 bytes python/tracking_data_container/catalog.py | 16 +- 24 files changed, 29 insertions(+), 175 deletions(-) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{00000-fb70b6ed-a7f1-472b-a66d-9b3e584dadd9.metadata.json => 00000-62e7e4e4-7d68-44e1-b7c9-ca6e892cf286.metadata.json} (85%) delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00001-e78c2066-f1ab-4b8b-adb2-38b3470d05ad.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00002-29eac68a-a4f0-4ac9-9cce-852bb885ebb5.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00003-d211893d-4899-4293-a3d4-319f62f14c74.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/2d702eea-f69f-42e3-942b-7052e0b73cd6-m0.avro delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/62719280-2ab0-4ae1-ba75-d57e508139d6-m0.avro delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/8c35a04a-3f37-4fe4-9d28-d33b25ff80bf-m0.avro delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/snap-6306903004315971787-0-62719280-2ab0-4ae1-ba75-d57e508139d6.avro delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/snap-6532061344649095069-0-8c35a04a-3f37-4fe4-9d28-d33b25ff80bf.avro delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/snap-8201725522326604393-0-2d702eea-f69f-42e3-942b-7052e0b73cd6.avro rename examples/idsse_warehouse/tracking/tracking_data/metadata/{00000-36633328-e8e4-41cb-b920-bd851aadf1c1.metadata.json => 00000-a67fd806-f108-4a16-b20b-39ce0ac55542.metadata.json} (91%) delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00001-ed728451-0fac-4795-b9f9-7b786ee6fc78.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00002-1af24f6c-2df4-4602-ba11-f6b72b778e9b.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00003-47b68db2-12df-4c75-8046-a6719215fa9a.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/17225286-c948-44c6-8594-1fb34271f9ba-m0.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/1b21427f-2ee9-4e4c-9e70-94eb1b10fbd5-m0.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/de5280f4-97b4-4cb5-8394-b034ba16c73a-m0.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-1893932036032482558-0-1b21427f-2ee9-4e4c-9e70-94eb1b10fbd5.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-3556048861821001954-0-de5280f4-97b4-4cb5-8394-b034ba16c73a.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-6329113174899808939-0-17225286-c948-44c6-8594-1fb34271f9ba.avro diff --git a/crates/tdc-core/src/datacontainer/core.rs b/crates/tdc-core/src/datacontainer/core.rs index 7ba684b..003d46e 100644 --- a/crates/tdc-core/src/datacontainer/core.rs +++ b/crates/tdc-core/src/datacontainer/core.rs @@ -432,18 +432,10 @@ impl TrackingDataContainer { } // 2. Select the best frame index column and validate required fields - let has_x = fields.iter().any(|f| f.name() == "x"); - let has_y = fields.iter().any(|f| f.name() == "y"); let has_period = fields .iter() .any(|f| f.name() == "period" || f.name() == "period_id"); - if !has_x || !has_y { - return Err(TrackingDataContainerErrorKind::InvalidParameters( - "Missing required spatial columns x or y".to_string(), - ) - .into()); - } if !has_period { return Err(TrackingDataContainerErrorKind::InvalidParameters( "Missing required column period or period_id".to_string(), diff --git a/examples/08-idsse-warehouse.ipynb b/examples/08-idsse-warehouse.ipynb index f995710..3b61754 100644 --- a/examples/08-idsse-warehouse.ipynb +++ b/examples/08-idsse-warehouse.ipynb @@ -137,17 +137,20 @@ "output_type": "stream", "text": [ "\n", - "Processing match DFL-MAT-J03WMX...\n", - " ✅ Parsed in 1.37s, Saved in 4.53s\n", - " ✅ Rows: 145,967\n", - "\n", - "Processing match DFL-MAT-J03WN1...\n", - " ✅ Parsed in 1.28s, Saved in 3.89s\n", - " ✅ Rows: 141,561\n", - "\n", - "Processing match DFL-MAT-J03WOH...\n", - " ✅ Parsed in 1.21s, Saved in 3.93s\n", - " ✅ Rows: 137,214\n" + "Processing match DFL-MAT-J03WMX...\n" + ] + }, + { + "ename": "ValueError", + "evalue": "Required field x contains null values", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mValueError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[4]\u001b[39m\u001b[32m, line 18\u001b[39m\n\u001b[32m 16\u001b[39m \u001b[38;5;66;03m# Save to Warehouse (melts to long format internally)\u001b[39;00m\n\u001b[32m 17\u001b[39m start = time.perf_counter()\n\u001b[32m---> \u001b[39m\u001b[32m18\u001b[39m \u001b[43mcatalog\u001b[49m\u001b[43m.\u001b[49m\u001b[43msave_container\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcontainer\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 19\u001b[39m save_time = time.perf_counter() - start\n\u001b[32m 21\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33m ✅ Parsed in \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mparse_time\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m.2f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33ms, Saved in \u001b[39m\u001b[38;5;132;01m{\u001b[39;00msave_time\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m.2f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33ms\u001b[39m\u001b[33m\"\u001b[39m)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/python/tracking_data_container/catalog.py:241\u001b[39m, in \u001b[36mTrackingDataCatalog.save_container\u001b[39m\u001b[34m(self, container, match_id, metadata)\u001b[39m\n\u001b[32m 238\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33m\"\u001b[39m\u001b[33mmatch_id must be provided if not present in container data\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 240\u001b[39m \u001b[38;5;66;03m# 1. Save tracking data and get unique entities (with constant attributes)\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m241\u001b[39m entities = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_save_tracking_data\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcontainer\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmatch_id\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 243\u001b[39m \u001b[38;5;66;03m# 2. Prepare metadata\u001b[39;00m\n\u001b[32m 244\u001b[39m final_metadata = metadata.copy() \u001b[38;5;28;01mif\u001b[39;00m metadata \u001b[38;5;28;01melse\u001b[39;00m {}\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/python/tracking_data_container/catalog.py:388\u001b[39m, in \u001b[36mTrackingDataCatalog._save_tracking_data\u001b[39m\u001b[34m(self, container, match_id)\u001b[39m\n\u001b[32m 386\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mRequired field \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfield\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m is missing from container\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 387\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m long_df[field].null_count() > \u001b[32m0\u001b[39m:\n\u001b[32m--> \u001b[39m\u001b[32m388\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mRequired field \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfield\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m contains null values\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 390\u001b[39m \u001b[38;5;66;03m# 4. Convert to Arrow and handle types\u001b[39;00m\n\u001b[32m 391\u001b[39m arrow_table = long_df.to_arrow()\n", + "\u001b[31mValueError\u001b[39m: Required field x contains null values" ] } ], @@ -190,22 +193,9 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Warehouse Summary:\n", - " - Match DFL-MAT-J03WMX: 165,259 rows\n", - " - Match DFL-MAT-J03WN1: 162,273 rows\n", - " - Match DFL-MAT-J03WOH: 157,790 rows\n", - "\n", - "Total rows in warehouse: 485,322\n" - ] - } - ], + "outputs": [], "source": [ "print(\"Warehouse Summary:\")\n", "total_rows = 0\n", @@ -234,62 +224,9 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Loading data for DFL-MAT-J03WMX...\n", - " ✅ Loaded and pivoted 165259 rows in 1.8728s\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "shape: (5, 424)
competition/idseason/idmatch/idframe_idperiodtimestampteams/away/players/DFL-OBJ-J01APO/player/idteams/away/players/DFL-OBJ-J01APO/team/idteams/away/players/DFL-OBJ-J01APO/xteams/away/players/DFL-OBJ-J01APO/yteams/away/players/DFL-OBJ-J01APO/zteams/away/players/DFL-OBJ-J01APO/velteams/away/players/DFL-OBJ-J01APO/accteams/away/players/DFL-OBJ-J01APO/entity_keyteams/away/players/DFL-OBJ-J01APO/nameteams/away/players/DFL-OBJ-J01APO/idteams/away/players/DFL-OBJ-J01APO/jersey_colourteams/home/players/DFL-OBJ-002G89/player/idteams/home/players/DFL-OBJ-002G89/team/idteams/home/players/DFL-OBJ-002G89/xteams/home/players/DFL-OBJ-002G89/yteams/home/players/DFL-OBJ-002G89/zteams/home/players/DFL-OBJ-002G89/velteams/home/players/DFL-OBJ-002G89/accteams/home/players/DFL-OBJ-002G89/entity_keyteams/home/players/DFL-OBJ-002G89/nameteams/home/players/DFL-OBJ-002G89/idteams/home/players/DFL-OBJ-002G89/jersey_colourteams/home/players/DFL-OBJ-00012X/player/idteams/home/players/DFL-OBJ-00012X/team/idteams/home/players/DFL-OBJ-00012X/xteams/home/players/DFL-OBJ-00012X/yteams/home/players/DFL-OBJ-00012X/zteams/home/players/DFL-OBJ-00012X/velteams/home/players/DFL-OBJ-00012X/accteams/home/players/DFL-OBJ-00012X/entity_keyteams/home/players/DFL-OBJ-00012X/nameteams/away/players/DFL-OBJ-0027G0/entity_keyteams/away/players/DFL-OBJ-0027G0/nameteams/away/players/DFL-OBJ-0027G0/idteams/away/players/DFL-OBJ-0027G0/jersey_colourteams/away/player/idteams/away/team/idteams/away/xteams/away/yteams/away/zteams/away/velteams/away/accteams/away/entity_keyteams/away/nameteams/away/idteams/away/jersey_colourteams/away/players/DFL-OBJ-0000RP/player/idteams/away/players/DFL-OBJ-0000RP/team/idteams/away/players/DFL-OBJ-0000RP/xteams/away/players/DFL-OBJ-0000RP/yteams/away/players/DFL-OBJ-0000RP/zteams/away/players/DFL-OBJ-0000RP/velteams/away/players/DFL-OBJ-0000RP/accteams/away/players/DFL-OBJ-0000RP/entity_keyteams/away/players/DFL-OBJ-0000RP/nameteams/away/players/DFL-OBJ-0000RP/idteams/away/players/DFL-OBJ-0000RP/jersey_colourteams/home/players/DFL-OBJ-000270/player/idteams/home/players/DFL-OBJ-000270/team/idteams/home/players/DFL-OBJ-000270/xteams/home/players/DFL-OBJ-000270/yteams/home/players/DFL-OBJ-000270/zteams/home/players/DFL-OBJ-000270/velteams/home/players/DFL-OBJ-000270/accteams/home/players/DFL-OBJ-000270/entity_keyteams/home/players/DFL-OBJ-000270/nameteams/home/players/DFL-OBJ-000270/idteams/home/players/DFL-OBJ-000270/jersey_colour
strstrstri64strdatetime[μs]strstrf32f32f32f64f64strstrstrstrstrstrf32f32f32f64f64strstrstrstrstrstrf32f32f32f64f64strstrstrstrstrstrstrstrf32f32f32f64f64strstrstrstrstrstrf32f32f32f64f64strstrstrstrstrstrf32f32f32f64f64strstrstrstr
nullnull"DFL-MAT-J03WMX"10000"first_half"2023-05-27 13:30:12.600"DFL-OBJ-J01APO""away"-13.825.719999null0.00.0"DFL-OBJ-J01APO|away"nullnullnullnullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-00012X""home"13.08-13.71null0.00.0"DFL-OBJ-00012X|home"null"DFL-OBJ-0027G0|away"nullnullnull"team:away""away"nullnullnullnullnull"team:away|away""FC Bayern München""DFL-CLU-00000G""#25282A"nullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-000270""home"-2.12-7.09null0.00.0"DFL-OBJ-000270|home"nullnullnull
nullnull"DFL-MAT-J03WMX"10001"first_half"2023-05-27 13:30:12.640"DFL-OBJ-J01APO""away"-13.7825.74null2.591.23"DFL-OBJ-J01APO|away"nullnullnullnullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-00012X""home"13.06-13.75null3.491.4"DFL-OBJ-00012X|home"null"DFL-OBJ-0027G0|away"nullnullnull"team:away""away"nullnullnullnullnull"team:away|away""FC Bayern München""DFL-CLU-00000G""#25282A"nullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-000270""home"-2.28-6.99null17.452.6"DFL-OBJ-000270|home"nullnullnull
nullnull"DFL-MAT-J03WMX"10002"first_half"2023-05-27 13:30:12.680"DFL-OBJ-J01APO""away"-13.7525.76null2.761.08"DFL-OBJ-J01APO|away"nullnullnullnullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-00012X""home"13.05-13.78null3.731.34"DFL-OBJ-00012X|home"null"DFL-OBJ-0027G0|away"nullnullnull"team:away""away"nullnullnullnullnull"team:away|away""FC Bayern München""DFL-CLU-00000G""#25282A"nullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-000270""home"-2.44-6.88null17.822.52"DFL-OBJ-000270|home"nullnullnull
nullnull"DFL-MAT-J03WMX"10003"first_half"2023-05-27 13:30:12.720"DFL-OBJ-J01APO""away"-13.7325.780001null2.890.91"DFL-OBJ-J01APO|away"nullnullnullnullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-00012X""home"13.03-13.82null3.951.24"DFL-OBJ-00012X|home"null"DFL-OBJ-0027G0|away"nullnullnull"team:away""away"nullnullnullnullnull"team:away|away""FC Bayern München""DFL-CLU-00000G""#25282A"nullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-000270""home"-2.61-6.78null18.192.44"DFL-OBJ-000270|home"nullnullnull
nullnull"DFL-MAT-J03WMX"10004"first_half"2023-05-27 13:30:12.760"DFL-OBJ-J01APO""away"-13.7125.809999null3.00.72"DFL-OBJ-J01APO|away"nullnullnullnullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-00012X""home"13.02-13.87null4.131.11"DFL-OBJ-00012X|home"null"DFL-OBJ-0027G0|away"nullnullnull"team:away""away"nullnullnullnullnull"team:away|away""FC Bayern München""DFL-CLU-00000G""#25282A"nullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-000270""home"-2.78-6.66null18.542.35"DFL-OBJ-000270|home"nullnullnull
" - ], - "text/plain": [ - "shape: (5, 424)\n", - "┌───────────┬───────────┬───────────┬──────────┬───┬───────────┬───────────┬───────────┬───────────┐\n", - "│ competiti ┆ season/id ┆ match/id ┆ frame_id ┆ … ┆ teams/hom ┆ teams/hom ┆ teams/hom ┆ teams/hom │\n", - "│ on/id ┆ --- ┆ --- ┆ --- ┆ ┆ e/players ┆ e/players ┆ e/players ┆ e/players │\n", - "│ --- ┆ str ┆ str ┆ i64 ┆ ┆ /DFL-OBJ- ┆ /DFL-OBJ- ┆ /DFL-OBJ- ┆ /DFL-OBJ- │\n", - "│ str ┆ ┆ ┆ ┆ ┆ 000… ┆ 000… ┆ 000… ┆ 000… │\n", - "│ ┆ ┆ ┆ ┆ ┆ --- ┆ --- ┆ --- ┆ --- │\n", - "│ ┆ ┆ ┆ ┆ ┆ str ┆ str ┆ str ┆ str │\n", - "╞═══════════╪═══════════╪═══════════╪══════════╪═══╪═══════════╪═══════════╪═══════════╪═══════════╡\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10000 ┆ … ┆ DFL-OBJ-0 ┆ null ┆ null ┆ null │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ 00270|hom ┆ ┆ ┆ │\n", - "│ ┆ ┆ ┆ ┆ ┆ e ┆ ┆ ┆ │\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10001 ┆ … ┆ DFL-OBJ-0 ┆ null ┆ null ┆ null │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ 00270|hom ┆ ┆ ┆ │\n", - "│ ┆ ┆ ┆ ┆ ┆ e ┆ ┆ ┆ │\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10002 ┆ … ┆ DFL-OBJ-0 ┆ null ┆ null ┆ null │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ 00270|hom ┆ ┆ ┆ │\n", - "│ ┆ ┆ ┆ ┆ ┆ e ┆ ┆ ┆ │\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10003 ┆ … ┆ DFL-OBJ-0 ┆ null ┆ null ┆ null │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ 00270|hom ┆ ┆ ┆ │\n", - "│ ┆ ┆ ┆ ┆ ┆ e ┆ ┆ ┆ │\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10004 ┆ … ┆ DFL-OBJ-0 ┆ null ┆ null ┆ null │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ 00270|hom ┆ ┆ ┆ │\n", - "│ ┆ ┆ ┆ ┆ ┆ e ┆ ┆ ┆ │\n", - "└───────────┴───────────┴───────────┴──────────┴───┴───────────┴───────────┴───────────┴───────────┘" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "target_match = pairs[0]['match_id']\n", "\n", @@ -306,57 +243,9 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'_entities': [{'entity_id': 'DFL-OBJ-J01N65', 'team_id': 'away'},\n", - " {'entity_id': 'DFL-OBJ-002G4A', 'team_id': 'home'},\n", - " {'entity_id': 'DFL-OBJ-J01D1W', 'team_id': 'home'},\n", - " {'entity_id': 'referee:DFL-OBJ-000141', 'team_id': None},\n", - " {'entity_id': 'DFL-OBJ-002G89', 'team_id': 'home'},\n", - " {'entity_id': 'DFL-OBJ-0002BO', 'team_id': 'home'},\n", - " {'entity_id': 'DFL-OBJ-0002F5', 'team_id': 'away'},\n", - " {'entity_id': 'DFL-OBJ-0027B6', 'team_id': 'home'},\n", - " {'entity_id': 'DFL-OBJ-0002AU', 'team_id': 'away'},\n", - " {'entity_id': 'DFL-OBJ-0026ZI', 'team_id': 'home'},\n", - " {'entity_id': 'DFL-OBJ-000191', 'team_id': 'away'},\n", - " {'entity_id': 'DFL-OBJ-0000RP', 'team_id': 'away'},\n", - " {'entity_id': 'DFL-OBJ-0002HE', 'team_id': 'home'},\n", - " {'entity_id': 'DFL-OBJ-0027KL', 'team_id': 'away'},\n", - " {'entity_id': 'referee:DFL-OBJ-00011W', 'team_id': None},\n", - " {'entity_id': 'DFL-OBJ-0000IA', 'team_id': 'away'},\n", - " {'entity_id': 'DFL-OBJ-J01B8N', 'team_id': 'away'},\n", - " {'entity_id': 'DFL-OBJ-J015S4', 'team_id': 'away'},\n", - " {'entity_id': 'DFL-OBJ-00012X', 'team_id': 'home'},\n", - " {'entity_id': 'DFL-OBJ-0027AX', 'team_id': 'home'},\n", - " {'entity_id': 'DFL-OBJ-0027G6', 'team_id': 'away'},\n", - " {'entity_id': 'DFL-OBJ-J017RE', 'team_id': 'away'},\n", - " {'entity_id': 'team:away', 'team_id': 'away'},\n", - " {'entity_id': 'DFL-OBJ-002GCR', 'team_id': 'away'},\n", - " {'entity_id': 'DFL-OBJ-000270', 'team_id': 'home'},\n", - " {'entity_id': 'referee:DFL-OBJ-00011J', 'team_id': None},\n", - " {'entity_id': 'DFL-OBJ-0028BD', 'team_id': 'home'},\n", - " {'entity_id': 'DFL-OBJ-0027V2', 'team_id': 'home'},\n", - " {'entity_id': 'DFL-OBJ-0026PM', 'team_id': 'away'},\n", - " {'entity_id': 'ball', 'team_id': None},\n", - " {'entity_id': 'DFL-OBJ-002GBW', 'team_id': 'home'},\n", - " {'entity_id': 'team:home', 'team_id': 'home'},\n", - " {'entity_id': 'DFL-OBJ-0002DR', 'team_id': 'away'},\n", - " {'entity_id': 'DFL-OBJ-J01APO', 'team_id': 'away'},\n", - " {'entity_id': 'DFL-OBJ-002GKO', 'team_id': 'home'},\n", - " {'entity_id': 'DFL-OBJ-J01BGM', 'team_id': 'home'},\n", - " {'entity_id': 'DFL-OBJ-J01DVC', 'team_id': 'home'},\n", - " {'entity_id': 'DFL-OBJ-0027G0', 'team_id': 'away'}]}" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "metadata = catalog.get_metadata(match_id=target_match)\n", "metadata" @@ -364,24 +253,9 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "ename": "FileNotFoundError", - "evalue": "No such file or directory (os error 2): ...rehouse/tracking/tracking_data/data/00000-0-2ce0e3ca-73c0-404e-9596-10570ff4a509.parquet (set POLARS_VERBOSE=1 to see full path)\n\nThis error occurred with the following context stack:\n\t[1] 'parquet scan'\n\t[2] 'sink'\n", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mFileNotFoundError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[7]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m start = time.perf_counter()\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m df = \u001b[43mpl\u001b[49m\u001b[43m.\u001b[49m\u001b[43mscan_parquet\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43midsse_warehouse/tracking/tracking_data/data/00000-0-2ce0e3ca-73c0-404e-9596-10570ff4a509.parquet\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcollect\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 4\u001b[39m query_time = time.perf_counter() - start\n\u001b[32m 5\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33m ✅ Loaded and pivoted \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mlen\u001b[39m(df)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m rows in \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mquery_time\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m.4f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33ms\u001b[39m\u001b[33m\"\u001b[39m)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/.venv/lib/python3.11/site-packages/polars/_utils/deprecation.py:97\u001b[39m, in \u001b[36mdeprecate_streaming_parameter..decorate..wrapper\u001b[39m\u001b[34m(*args, **kwargs)\u001b[39m\n\u001b[32m 93\u001b[39m kwargs[\u001b[33m\"\u001b[39m\u001b[33mengine\u001b[39m\u001b[33m\"\u001b[39m] = \u001b[33m\"\u001b[39m\u001b[33min-memory\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 95\u001b[39m \u001b[38;5;28;01mdel\u001b[39;00m kwargs[\u001b[33m\"\u001b[39m\u001b[33mstreaming\u001b[39m\u001b[33m\"\u001b[39m]\n\u001b[32m---> \u001b[39m\u001b[32m97\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunction\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/.venv/lib/python3.11/site-packages/polars/lazyframe/opt_flags.py:328\u001b[39m, in \u001b[36mforward_old_opt_flags..decorate..wrapper\u001b[39m\u001b[34m(*args, **kwargs)\u001b[39m\n\u001b[32m 325\u001b[39m optflags = cb(optflags, kwargs.pop(key)) \u001b[38;5;66;03m# type: ignore[no-untyped-call,unused-ignore]\u001b[39;00m\n\u001b[32m 327\u001b[39m kwargs[\u001b[33m\"\u001b[39m\u001b[33moptimizations\u001b[39m\u001b[33m\"\u001b[39m] = optflags\n\u001b[32m--> \u001b[39m\u001b[32m328\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunction\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/.venv/lib/python3.11/site-packages/polars/lazyframe/frame.py:2429\u001b[39m, in \u001b[36mLazyFrame.collect\u001b[39m\u001b[34m(self, type_coercion, predicate_pushdown, projection_pushdown, simplify_expression, slice_pushdown, comm_subplan_elim, comm_subexpr_elim, cluster_with_columns, collapse_joins, no_optimization, engine, background, optimizations, **_kwargs)\u001b[39m\n\u001b[32m 2427\u001b[39m \u001b[38;5;66;03m# Only for testing purposes\u001b[39;00m\n\u001b[32m 2428\u001b[39m callback = _kwargs.get(\u001b[33m\"\u001b[39m\u001b[33mpost_opt_callback\u001b[39m\u001b[33m\"\u001b[39m, callback)\n\u001b[32m-> \u001b[39m\u001b[32m2429\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m wrap_df(\u001b[43mldf\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcollect\u001b[49m\u001b[43m(\u001b[49m\u001b[43mengine\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcallback\u001b[49m\u001b[43m)\u001b[49m)\n", - "\u001b[31mFileNotFoundError\u001b[39m: No such file or directory (os error 2): ...rehouse/tracking/tracking_data/data/00000-0-2ce0e3ca-73c0-404e-9596-10570ff4a509.parquet (set POLARS_VERBOSE=1 to see full path)\n\nThis error occurred with the following context stack:\n\t[1] 'parquet scan'\n\t[2] 'sink'\n" - ] - } - ], + "outputs": [], "source": [ "start = time.perf_counter()\n", "\n", diff --git a/examples/idsse_warehouse/catalog.db b/examples/idsse_warehouse/catalog.db index 5dd3c79decc78f46e971b2ab6380ffcc5e3468e4..146187d1168342f190f8d595e162fea0c295b762 100644 GIT binary patch delta 166 zcmZozz}T>Wae_1>>qHr6M%Il93;DSj7#NuN!Wj5<_`)^|DtPg6H3l=WOUr6DWiw1Z zpf5XlkzBQ%fv%ZRs(GqOs)??7ikXG3iAk!VZjyPjrEYSfS*nGlQF5A*g_&M%YDr>B zVo9Q2R&jn_V<02Y81<%1hROG2rl}d|CYqV2rC1o4>82SP0F6jAG}BEoGDy-jwoFbn RNK7_0H8nBX{7IhA001SpEg%2@ delta 291 zcmY+;Jx;?w5J2H|XpkHks0}s+0#Qul9gp{?<_;7zWOmmpg$O7}1wx9P3xF&k%5a1z zhu{FY2`9j94)C77v##$h>HExXm!Fu0AyaqJ2?g;+rod#y5TN+&i7wnQiG8n>5Yt z-MRmB=662sxrfz}$C@|$+At~O{~AvUblfFEm*7(`(5R3R43k*8FLWJ0DVVy@C1wO0 zm)R3#OtoPJ)-s7brhNjwd*toF^awl-Dr6!PZk6J#np1WOHL2;?D+w6b@?!-#R`3b- z*SNVo-drXW>zHC?1TujuOuY0u9nuJnA`Znxhk~Sto{tDQVUmn}U_cnV(Pb-$NhHycC59 z0cOqdfIo_Zgdj>`co0Mo)4-2^<<6rVJRC+C38qbji402Yu(9h8JCw&{K{VVZR<|7& z?7*^^tmQzki|_$_#(_nJF2sIHOF||PTO*1clvU!@cq6Y@)-Wxi_{(ayuUDIA1~X9O ztW*_TZ3q^ESR*9L3Ey}W7(+(`enJn}MPYs*7RWBdVxK{dr?Gzs(g}dp#EsK%F&(Zo zluL#iDw0`k_;cEF?06^^CNIb-hhoF^g=i zIsz;yqcZTsoWx!yB77t&QN#j`Xy95T6TUV^D)xzv00|u?jCEMdh!t}hNmzP{<^)hg zARhGrh@qHEq;*&%RqK7c43J}>P{BuLXpAvVo8Z)a^Btm?wxW(xQd`SnE}2KX8moo| z9EvT(G!WBZWAQu~4O(315Dt4qiAH`O{D>T{?y?<&?exk@iZ#gGS4O;{J zXhU#vxxz#d*3*rPEreBg2)isqAsuAuh&u4cfO3|QAG+z6(&6F`g$c13WhZDaI9dmk zoq$rx#emk1vUBYyrChyH^4iHjVro{PL*L+s2g0_F`fy*GEjw0}yB<-muT4^uK(~j84nj3+%cX_l+o;p%T{n&C= zh_gyV3|#l&5bJs{Nvxa7kI+q}6}st?5al|yQ_gBBh;XPX(OoA_l7)d@P7H2w#}qI4xF7sMiExCTW| zv&kx&H^R|s*>n6TbAe(NhM>rgRKmjr0>cyIaN$|(;VMhD#>2g9^RS2Au3^IPQNSio zm@{-jiB~plf;?voMIN&GE&i;7O$yfp81DSG8t9G(%sm~7huzH6-KTx2Q<7gmmWz1r zKA2TqkNE7;3&*q%Cg3UpCfG#x``(AYE8E&$HuRyP{LRX_>9Zr3u3mku5;Po_V5Zf5W?tfByPU z*;~EYU+x~<)z)`<_uB}XxGIFJ4s+Ef+duXIGGO63Mv+CtzlY;|;Gld&Z|Ixa&^{;1d{A28Vv1Q}^ zEj{NpeY5FIOWWq|=9VX0woNvO&s;y))P&wY)%*Q-w_lr|7x&Hocs>7X$Gfk7TG;i< jo~t{)+#!EG_``u0uFbUW-_!kV_fBcUhAj=nS}Oko13RZl diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/62719280-2ab0-4ae1-ba75-d57e508139d6-m0.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/62719280-2ab0-4ae1-ba75-d57e508139d6-m0.avro deleted file mode 100644 index 3d73cb8cfc3d57e53e1f0f75768fd6d97a42e86c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4510 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 diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-6532061344649095069-0-8c35a04a-3f37-4fe4-9d28-d33b25ff80bf.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-6532061344649095069-0-8c35a04a-3f37-4fe4-9d28-d33b25ff80bf.avro deleted file mode 100644 index 95ee30e8ad133bbd746c37b4ff3af96dbb2c666a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1950 zcmb7ETWkzb7*?uLQJP>SY#2l8L6@1%Ub^ZMf-I3pUE)$q&d!{*!_1s9bGEgq5|<4T zLWsH~(h!jd8kdmJh#;=1OH!fYK~fs6l_(L47iYGeHSM&E*K_{w`~Ls?FAKx#vIe;f z6=+5jVUHvh_T`ho#AP#hR-zzs2| z3ecm*lrYr$a_$Jy6@WeEP&Xu`_AMF)%5^kJKq3?di!dSs3|*-wLx@5J)Q+g6aa@D+ z6pGfNfOL^Ar09qQWzisSWtFrFl+>>RR4D=t%xeHg(?KME66RwVg0grFtV`-zGeks! zNxT3F0F!K}rHkfK6%vk8a~D%7eMnaoQaxzMP+i-D3Tso55z**^CZ1u*)Zq;c3Ovco zD#gmO$cC3x+!Zb$6%)_9gV;bOkktUbqX7=E&jxm8tVkr+&KCB@;LpeaL=lR-H6}gP z%>4sCBFWI;1r&3Z&v+_$t)KWo3&OMeT}5+8j}1*6KD(NJuqSjY zbY_mMJi3zDjhY&L2ai)N-#KIu(O%@gFmO|@TZtH+dqtHNtCY2AKN(oa(?cC{c~n*Z@Ng;9x8nCv-#5< XZS&{0_?OtUEoI4@Sug7zba&}DL28wd diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-8201725522326604393-0-2d702eea-f69f-42e3-942b-7052e0b73cd6.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-8201725522326604393-0-2d702eea-f69f-42e3-942b-7052e0b73cd6.avro deleted file mode 100644 index 9cd86678dad2fe4c661316c2103f1d88af4e262d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1826 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 O%Pm)%ouq0zv*Z7%*R|gO diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-1893932036032482558-0-1b21427f-2ee9-4e4c-9e70-94eb1b10fbd5.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-1893932036032482558-0-1b21427f-2ee9-4e4c-9e70-94eb1b10fbd5.avro deleted file mode 100644 index d5fab2cf5dba61d54e3d2b686ea2403b67ce3c43..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1849 zcmb7ET}TvB7}cdBOSXdiiBzUB^+8u=cU|2Llr$0-grv|?vElB_U3YALwwb%Gi)=4F zg-<0>u#hAbgq1{w6%?#l1iqwDNKpwD5)pzFNiTJ0cV`_P9qskr@0{~}=leP4>&eW{ zRYVX}0ur)uY^mMtaeAB%%2`c09aV0J%jG^81TqwnO&7LZ4Do!C0?&q^$UvJI5`0j$ z*bDrUEC6Kdg|fm)qQ$Wn^vY5tBe9UNve3^11alKl3KM_=xR=*YqDT;uhX_)Jj3l#U z1wr~b$g>K5>+B~*AS8YTAaZ`FAUX(8ARUAg#$hgs!YGYLK{6+Ht0An!sKm_@0}#%J zXg%&F!W;?^jVFkNJ|v?R#2Okr6uZ|@vFlWL32UrCW0Mf4>d;CMGBnOiD@DuFp~G_` zS_x+)5n+>8gX}eCAguv(vH=FLO9wV)EO0p2R14Q*s5WH)EDKp$n~Sw-c5VXi=Xj{l zj1)4KPgUvg2G3w0ry$&H+KfT(fq{XAJVdwzGXlK@27;smvJ_sY0K2~F#ulV2Fkvva zP^-X%K<}Y3h4kDitH+1Z&0Z&|Zaj`yURWT@AfjThhk|+^Qp@PZPaJ}Ry1~oQ=IgCa zKj1_Kk6}V3c>^>?k8TKjlEgzG8kZ*t$3HkYaY0gOyP8kxIHao}HJUE`i4PM9@d&4@ zs;^jTUJ65*_TkYqtg~HrbacEC1Ql`v)=)6GF01ayY=Tu4(oUr8qTFOOx`Fl?7@-A- z01FT>Sfw-iKgHAcS=}TunsJJ{xM=Inqu>5q`X2vXyU+5+(lt=KV}iaymX#-}3Bhbl1nYcP3kSIXioJe_KyU zN!9RU*PVeJM{9#~|nBqmpdBE}66Axcs9l05=S+ccT%j@R6 zpFW=}zh1Yy#ac9-VY{-eU}o@I?A0T$GG5u%@q=k7n7{wNXmoz+!pSVp2kG*7@#vc; aPcsX98^;!^OP_!HTlOVu%Y)|C_51^`eq>bu diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-3556048861821001954-0-de5280f4-97b4-4cb5-8394-b034ba16c73a.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-3556048861821001954-0-de5280f4-97b4-4cb5-8394-b034ba16c73a.avro deleted file mode 100644 index 9473c53ca6b427be62fed262b03b3ec1add7be5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1936 zcmb7ETSyd97|t*VilW@MD2ZvZD9D=Gb=KV?(v;|KTA3s<9B0nDv(9CmIh$>|lte{B zR1{KCP(eK;RvJ-31%*A7W++`BqR_mcr9o*$5S>|_ZFF?4*K_{w`~Ls?FIfftxXE@+ z1_8~dpk#qx=wuidmEv~0Xt#r=DB8oMBn5y9Wt40Vqum~-$LXM)F3RahaXT2sJy(Op zK`3)DSq@4CP#y0`^eU=_bt9aalK zj>VZ7r5IT{On5;?L*bkvBkcKb5SpNqj| zbmJ!hK}p}>ztOSRTc3U)$QmBQh|0hXFc>|iAt+E35dzt|JOgn2gM%X%WDrg3`3xS1 zXceSEvkQOX!vumN!s+VjLzY@pN}$RX;L)_KGi^FLCf*Q&8aW1QFc27()pTV3fmIhW zPNd3EZZaGmL%RruSP3G)0|YEqnT-BV@#uZlH;D|76Z<^cgLyyJ2B&SVs(8nqkFSV- z{QUmz+PZwCK*8da1_~sJQ;M_TB}i@#nSq-FvvA%vye7b!*1ioa|ISRCLSJP<5dA zYW_-B@MYJ%uF~aAaO?5bP*>v7W4A7^3xDh^{8IX4gTL?f$sF6{%qzr^>~hDfhr~wP zg^4r#cl|9}G8X54d+}=OA^Lsq=4I_2S+~D;I{W*}Zr7desQF!acuqdG^L_cIX+2W? zcSfX2&T2!2wpT}e* zl2}%hua}&klA6qylA4y2SduCR7Aj88NX<=LCtj^oQdy9yWTl`~l$xAhl%k}gpp=%G znv+rt6pOA_%1g`z%jPELWu~PTm&6w&mSn&c0o@N&W@=yn)CDuAxTGjEFCC;LB|jNt zuupz+VhPYS3ZX%s3gww4847N}3Sj$Fm1=bqu-K84nwO5n7DFUka`J(8py&pMSxF|y zp7`Q|)Z}=e%V1uCxzY$pe`a0@s`ljkyb@rTqiZrW$EOJv0r8Lsz|d@pq#4u2xtTz> zLZc9u31)arNK8pdO^Jsk6KsAqCPIH&W=?8xd~$v%ruPj@ko04OS!zXQaS1T8w-PEx96V40*WI(^^SV`2wG7bHvG z`N`JCdi3H(FJ?ypwg;wdHm&Oo-o3pW_wJe>hf>m(z%AUntBao*oNV1|eCsz~SlG5E z%{4hWJQ9iu8eC>={s$xLdN^vdm%uZ8JO+PUWCmZ_(gq`u#wKKm_Onn~v-p6uP15^V3DEC~5=dx`Iv zr{@nbUH^J_15eZk0gLOY9JL1j@`6nuUoUH&)!sDXF~0N&T4u)6vH4r_L`T7Z=&AQ_+5X zKw46M?Z${VZ_C+_pTEw3O2|Gg|G)aW4IgbRm4fv)zwbKeDso_Awt$3%f`!(y8N#bi o{8ydlet*vS%gv`BUR`ircjNzm-|RQ;vYf~HlS$3-DSD^^07RIpa{vGU diff --git a/python/tracking_data_container/catalog.py b/python/tracking_data_container/catalog.py index e08f70f..760dfc9 100644 --- a/python/tracking_data_container/catalog.py +++ b/python/tracking_data_container/catalog.py @@ -143,7 +143,7 @@ def _create_tracking_table(self) -> None: except NoSuchTableError: pass # Create new table - # Define the requested schema with specific order and requirements. + # Define the requested schema. x, y, z are optional. schema = Schema( NestedField(1, "competition_id", StringType(), required=False), NestedField(2, "season_id", StringType(), required=False), @@ -154,8 +154,8 @@ def _create_tracking_table(self) -> None: NestedField(7, "entity_type", StringType(), required=True), NestedField(8, "entity_id", StringType(), required=True), NestedField(9, "team_id", StringType(), required=False), - NestedField(10, "x", FloatType(), required=True), - NestedField(11, "y", FloatType(), required=True), + NestedField(10, "x", FloatType(), required=False), + NestedField(11, "y", FloatType(), required=False), NestedField(12, "z", FloatType(), required=False), ) @@ -326,7 +326,6 @@ def _save_tracking_data( # --- Constant Extraction --- # Identify attributes that are constant for each entity - # These will be stored in metadata instead of the physical table ignore_for_constants = { "match_id", "frame_id", @@ -344,20 +343,16 @@ def _save_tracking_data( entities_with_constants = [] if potential_constants: - # Group by entity and check uniqueness of potential constants agg_exprs = [pl.col(c).n_unique().alias(f"{c}_count") for c in potential_constants] agg_vals = [pl.col(c).first().alias(f"{c}_val") for c in potential_constants] stats = long_df.group_by(["entity_id", "team_id"]).agg(agg_exprs + agg_vals) - # We'll drop columns from the physical table if they are constant for ALL entities columns_to_drop = [] for c in potential_constants: if stats[f"{c}_count"].max() == 1: columns_to_drop.append(c) - # Prepare the entity list for metadata - # For each entity, we store the values of the dropped columns entities_with_constants = ( stats.select(["entity_id", "team_id"] + [f"{c}_val" for c in columns_to_drop]) .rename({f"{c}_val": c for c in columns_to_drop}) @@ -370,7 +365,8 @@ def _save_tracking_data( else: entities_with_constants = long_df.select(["entity_id", "team_id"]).unique().to_dicts() - # 3. Validation of required fields + # 3. Validation of required fields. + # x, y, z are explicitly allowed to be null. required_fields = [ "match_id", "frame_id", @@ -378,8 +374,6 @@ def _save_tracking_data( "timestamp", "entity_type", "entity_id", - "x", - "y", ] for field in required_fields: if field not in long_df.columns: From 33ef840a128c789b8ab19fe8aa97c3447ba4fb27 Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Wed, 8 Apr 2026 15:42:57 +0200 Subject: [PATCH 12/21] feat: enforce x/y column presence while allowing null values --- crates/tdc-core/src/datacontainer/core.rs | 8 + examples/08-idsse-warehouse.ipynb | 317 ++++++++++++++++-- examples/idsse_warehouse/catalog.db | Bin 20480 -> 0 bytes ...-7d68-44e1-b7c9-ca6e892cf286.metadata.json | 1 - ...-f108-4a16-b20b-39ce0ac55542.metadata.json | 1 - profile_warehouse/catalog.db | Bin 20480 -> 0 bytes ...-582c-4563-a6f7-935a13162b9f.metadata.json | 1 - ...-1c1a-4098-903a-41eec5ee454e.metadata.json | 1 - ...67bf9b-f807-48db-aae8-3a65e2db3451-m0.avro | Bin 4487 -> 0 bytes ...-3967bf9b-f807-48db-aae8-3a65e2db3451.avro | Bin 1819 -> 0 bytes ...-a026-4806-acfa-74075182b917.metadata.json | 1 - ...-d534-4ba2-b39c-44bd8e3935da.metadata.json | 1 - ...f6aa22-f608-4e8d-af73-eee758db29a1-m0.avro | Bin 5445 -> 0 bytes ...-acf6aa22-f608-4e8d-af73-eee758db29a1.avro | Bin 1829 -> 0 bytes python/tests/test_catalog.py | 4 +- python/tracking_data_container/catalog.py | 13 +- 16 files changed, 315 insertions(+), 33 deletions(-) delete mode 100644 examples/idsse_warehouse/catalog.db delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00000-62e7e4e4-7d68-44e1-b7c9-ca6e892cf286.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00000-a67fd806-f108-4a16-b20b-39ce0ac55542.metadata.json delete mode 100644 profile_warehouse/catalog.db delete mode 100644 profile_warehouse/tracking/match_metadata/metadata/00000-64842057-582c-4563-a6f7-935a13162b9f.metadata.json delete mode 100644 profile_warehouse/tracking/match_metadata/metadata/00001-8e94e659-1c1a-4098-903a-41eec5ee454e.metadata.json delete mode 100644 profile_warehouse/tracking/match_metadata/metadata/3967bf9b-f807-48db-aae8-3a65e2db3451-m0.avro delete mode 100644 profile_warehouse/tracking/match_metadata/metadata/snap-9142401099081597578-0-3967bf9b-f807-48db-aae8-3a65e2db3451.avro delete mode 100644 profile_warehouse/tracking/tracking_data/metadata/00000-ccf96e5c-a026-4806-acfa-74075182b917.metadata.json delete mode 100644 profile_warehouse/tracking/tracking_data/metadata/00001-6e5882b4-d534-4ba2-b39c-44bd8e3935da.metadata.json delete mode 100644 profile_warehouse/tracking/tracking_data/metadata/acf6aa22-f608-4e8d-af73-eee758db29a1-m0.avro delete mode 100644 profile_warehouse/tracking/tracking_data/metadata/snap-2502839378513990955-0-acf6aa22-f608-4e8d-af73-eee758db29a1.avro diff --git a/crates/tdc-core/src/datacontainer/core.rs b/crates/tdc-core/src/datacontainer/core.rs index 003d46e..7ba684b 100644 --- a/crates/tdc-core/src/datacontainer/core.rs +++ b/crates/tdc-core/src/datacontainer/core.rs @@ -432,10 +432,18 @@ impl TrackingDataContainer { } // 2. Select the best frame index column and validate required fields + let has_x = fields.iter().any(|f| f.name() == "x"); + let has_y = fields.iter().any(|f| f.name() == "y"); let has_period = fields .iter() .any(|f| f.name() == "period" || f.name() == "period_id"); + if !has_x || !has_y { + return Err(TrackingDataContainerErrorKind::InvalidParameters( + "Missing required spatial columns x or y".to_string(), + ) + .into()); + } if !has_period { return Err(TrackingDataContainerErrorKind::InvalidParameters( "Missing required column period or period_id".to_string(), diff --git a/examples/08-idsse-warehouse.ipynb b/examples/08-idsse-warehouse.ipynb index 3b61754..ae28a5d 100644 --- a/examples/08-idsse-warehouse.ipynb +++ b/examples/08-idsse-warehouse.ipynb @@ -137,20 +137,17 @@ "output_type": "stream", "text": [ "\n", - "Processing match DFL-MAT-J03WMX...\n" - ] - }, - { - "ename": "ValueError", - "evalue": "Required field x contains null values", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mValueError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[4]\u001b[39m\u001b[32m, line 18\u001b[39m\n\u001b[32m 16\u001b[39m \u001b[38;5;66;03m# Save to Warehouse (melts to long format internally)\u001b[39;00m\n\u001b[32m 17\u001b[39m start = time.perf_counter()\n\u001b[32m---> \u001b[39m\u001b[32m18\u001b[39m \u001b[43mcatalog\u001b[49m\u001b[43m.\u001b[49m\u001b[43msave_container\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcontainer\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 19\u001b[39m save_time = time.perf_counter() - start\n\u001b[32m 21\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33m ✅ Parsed in \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mparse_time\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m.2f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33ms, Saved in \u001b[39m\u001b[38;5;132;01m{\u001b[39;00msave_time\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m.2f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33ms\u001b[39m\u001b[33m\"\u001b[39m)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/python/tracking_data_container/catalog.py:241\u001b[39m, in \u001b[36mTrackingDataCatalog.save_container\u001b[39m\u001b[34m(self, container, match_id, metadata)\u001b[39m\n\u001b[32m 238\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33m\"\u001b[39m\u001b[33mmatch_id must be provided if not present in container data\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 240\u001b[39m \u001b[38;5;66;03m# 1. Save tracking data and get unique entities (with constant attributes)\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m241\u001b[39m entities = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_save_tracking_data\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcontainer\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmatch_id\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 243\u001b[39m \u001b[38;5;66;03m# 2. Prepare metadata\u001b[39;00m\n\u001b[32m 244\u001b[39m final_metadata = metadata.copy() \u001b[38;5;28;01mif\u001b[39;00m metadata \u001b[38;5;28;01melse\u001b[39;00m {}\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/python/tracking_data_container/catalog.py:388\u001b[39m, in \u001b[36mTrackingDataCatalog._save_tracking_data\u001b[39m\u001b[34m(self, container, match_id)\u001b[39m\n\u001b[32m 386\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mRequired field \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfield\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m is missing from container\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 387\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m long_df[field].null_count() > \u001b[32m0\u001b[39m:\n\u001b[32m--> \u001b[39m\u001b[32m388\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mRequired field \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfield\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m contains null values\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 390\u001b[39m \u001b[38;5;66;03m# 4. Convert to Arrow and handle types\u001b[39;00m\n\u001b[32m 391\u001b[39m arrow_table = long_df.to_arrow()\n", - "\u001b[31mValueError\u001b[39m: Required field x contains null values" + "Processing match DFL-MAT-J03WMX...\n", + " ✅ Parsed in 20.25s, Saved in 14.32s\n", + " ✅ Rows: 145,967\n", + "\n", + "Processing match DFL-MAT-J03WN1...\n", + " ✅ Parsed in 19.48s, Saved in 13.57s\n", + " ✅ Rows: 141,561\n", + "\n", + "Processing match DFL-MAT-J03WOH...\n", + " ✅ Parsed in 13.43s, Saved in 10.51s\n", + " ✅ Rows: 137,214\n" ] } ], @@ -193,9 +190,22 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Warehouse Summary:\n", + " - Match DFL-MAT-J03WMX: 165,259 rows\n", + " - Match DFL-MAT-J03WN1: 162,273 rows\n", + " - Match DFL-MAT-J03WOH: 157,790 rows\n", + "\n", + "Total rows in warehouse: 485,322\n" + ] + } + ], "source": [ "print(\"Warehouse Summary:\")\n", "total_rows = 0\n", @@ -224,9 +234,57 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading data for DFL-MAT-J03WMX...\n", + " ✅ Loaded and pivoted 165259 rows in 8.6644s\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "shape: (5, 424)
competition/idseason/idmatch/idframe_idperiodtimestampteams/away/players/DFL-OBJ-J01N65/player/idteams/away/players/DFL-OBJ-J01N65/team/idteams/away/players/DFL-OBJ-J01N65/xteams/away/players/DFL-OBJ-J01N65/yteams/away/players/DFL-OBJ-J01N65/zteams/away/players/DFL-OBJ-J01N65/velteams/away/players/DFL-OBJ-J01N65/accteams/away/players/DFL-OBJ-J01N65/entity_keyteams/home/players/DFL-OBJ-0002BO/player/idteams/home/players/DFL-OBJ-0002BO/team/idteams/home/players/DFL-OBJ-0002BO/xteams/home/players/DFL-OBJ-0002BO/yteams/home/players/DFL-OBJ-0002BO/zteams/home/players/DFL-OBJ-0002BO/velteams/home/players/DFL-OBJ-0002BO/accteams/home/players/DFL-OBJ-0002BO/entity_keyteams/home/players/DFL-OBJ-0027AX/player/idteams/home/players/DFL-OBJ-0027AX/team/idteams/home/players/DFL-OBJ-0027AX/xteams/home/players/DFL-OBJ-0027AX/yteams/home/players/DFL-OBJ-0027AX/zteams/home/players/DFL-OBJ-0027AX/velteams/home/players/DFL-OBJ-0027AX/accteams/home/players/DFL-OBJ-0027AX/entity_keyteams/home/players/DFL-OBJ-J01DVC/player/idteams/home/players/DFL-OBJ-J01DVC/team/idteams/home/players/DFL-OBJ-J01DVC/xteams/home/players/DFL-OBJ-J01DVC/yteams/home/players/DFL-OBJ-J01DVC/zteams/home/players/DFL-OBJ-J01DVC/velteams/home/players/DFL-OBJ-J01DVC/accteams/home/jersey_colourball/nameball/idball/jersey_colourteams/home/players/DFL-OBJ-002G89/nameteams/home/players/DFL-OBJ-002G89/idteams/home/players/DFL-OBJ-002G89/jersey_colourteams/home/players/DFL-OBJ-002G4A/nameteams/home/players/DFL-OBJ-002G4A/idteams/home/players/DFL-OBJ-002G4A/jersey_colourreferees/DFL-OBJ-00011W/namereferees/DFL-OBJ-00011W/idreferees/DFL-OBJ-00011W/jersey_colourteams/home/players/DFL-OBJ-0027V2/nameteams/home/players/DFL-OBJ-0027V2/idteams/home/players/DFL-OBJ-0027V2/jersey_colourteams/away/players/DFL-OBJ-0027KL/nameteams/away/players/DFL-OBJ-0027KL/idteams/away/players/DFL-OBJ-0027KL/jersey_colourteams/away/players/DFL-OBJ-000191/nameteams/away/players/DFL-OBJ-000191/idteams/away/players/DFL-OBJ-000191/jersey_colourteams/home/players/DFL-OBJ-0027AX/nameteams/home/players/DFL-OBJ-0027AX/idteams/home/players/DFL-OBJ-0027AX/jersey_colourteams/away/players/DFL-OBJ-0002F5/nameteams/away/players/DFL-OBJ-0002F5/idteams/away/players/DFL-OBJ-0002F5/jersey_colourteams/home/players/DFL-OBJ-00012X/nameteams/home/players/DFL-OBJ-00012X/idteams/home/players/DFL-OBJ-00012X/jersey_colourteams/away/players/DFL-OBJ-0000IA/nameteams/away/players/DFL-OBJ-0000IA/idteams/away/players/DFL-OBJ-0000IA/jersey_colourteams/away/players/DFL-OBJ-0027G0/nameteams/away/players/DFL-OBJ-0027G0/idteams/away/players/DFL-OBJ-0027G0/jersey_colour
strstrstri64strdatetime[μs]strstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64strnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnull
nullnull"DFL-MAT-J03WMX"10000"first_half"2023-05-27 13:30:12.600nullnullnullnullnullnullnullnull"DFL-OBJ-0002BO""home"17.2620.85null0.00.0"DFL-OBJ-0002BO|home""DFL-OBJ-0027AX""home"6.95.12null0.00.0"DFL-OBJ-0027AX|home"nullnullnullnullnullnullnull"#F4F5F0"nullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnull
nullnull"DFL-MAT-J03WMX"10001"first_half"2023-05-27 13:30:12.640nullnullnullnullnullnullnullnull"DFL-OBJ-0002BO""home"17.2320.879999null4.431.08"DFL-OBJ-0002BO|home""DFL-OBJ-0027AX""home"6.825.1null8.943.24"DFL-OBJ-0027AX|home"nullnullnullnullnullnullnull"#F4F5F0"nullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnull
nullnull"DFL-MAT-J03WMX"10002"first_half"2023-05-27 13:30:12.680nullnullnullnullnullnullnullnull"DFL-OBJ-0002BO""home"17.1820.91null4.61.07"DFL-OBJ-0002BO|home""DFL-OBJ-0027AX""home"6.725.08null9.393.06"DFL-OBJ-0027AX|home"nullnullnullnullnullnullnull"#F4F5F0"nullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnull
nullnull"DFL-MAT-J03WMX"10003"first_half"2023-05-27 13:30:12.720nullnullnullnullnullnullnullnull"DFL-OBJ-0002BO""home"17.1520.940001null4.741.03"DFL-OBJ-0002BO|home""DFL-OBJ-0027AX""home"6.625.05null9.822.84"DFL-OBJ-0027AX|home"nullnullnullnullnullnullnull"#F4F5F0"nullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnull
nullnull"DFL-MAT-J03WMX"10004"first_half"2023-05-27 13:30:12.760nullnullnullnullnullnullnullnull"DFL-OBJ-0002BO""home"17.11000120.98null4.880.98"DFL-OBJ-0002BO|home""DFL-OBJ-0027AX""home"6.55.02null10.212.6"DFL-OBJ-0027AX|home"nullnullnullnullnullnullnull"#F4F5F0"nullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnull
" + ], + "text/plain": [ + "shape: (5, 424)\n", + "┌───────────┬───────────┬───────────┬──────────┬───┬───────────┬───────────┬───────────┬───────────┐\n", + "│ competiti ┆ season/id ┆ match/id ┆ frame_id ┆ … ┆ teams/awa ┆ teams/awa ┆ teams/awa ┆ teams/awa │\n", + "│ on/id ┆ --- ┆ --- ┆ --- ┆ ┆ y/players ┆ y/players ┆ y/players ┆ y/players │\n", + "│ --- ┆ str ┆ str ┆ i64 ┆ ┆ /DFL-OBJ- ┆ /DFL-OBJ- ┆ /DFL-OBJ- ┆ /DFL-OBJ- │\n", + "│ str ┆ ┆ ┆ ┆ ┆ 000… ┆ 002… ┆ 002… ┆ 002… │\n", + "│ ┆ ┆ ┆ ┆ ┆ --- ┆ --- ┆ --- ┆ --- │\n", + "│ ┆ ┆ ┆ ┆ ┆ null ┆ null ┆ null ┆ null │\n", + "╞═══════════╪═══════════╪═══════════╪══════════╪═══╪═══════════╪═══════════╪═══════════╪═══════════╡\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10000 ┆ … ┆ null ┆ null ┆ null ┆ null │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ ┆ ┆ ┆ │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10001 ┆ … ┆ null ┆ null ┆ null ┆ null │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ ┆ ┆ ┆ │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10002 ┆ … ┆ null ┆ null ┆ null ┆ null │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ ┆ ┆ ┆ │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10003 ┆ … ┆ null ┆ null ┆ null ┆ null │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ ┆ ┆ ┆ │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10004 ┆ … ┆ null ┆ null ┆ null ┆ null │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ ┆ ┆ ┆ │\n", + "└───────────┴───────────┴───────────┴──────────┴───┴───────────┴───────────┴───────────┴───────────┘" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "target_match = pairs[0]['match_id']\n", "\n", @@ -243,9 +301,209 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "{'_entities': [{'entity_id': 'DFL-OBJ-002GCR',\n", + " 'team_id': 'away',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-0028BD',\n", + " 'team_id': 'home',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-J015S4',\n", + " 'team_id': 'away',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-J01BGM',\n", + " 'team_id': 'home',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-J017RE',\n", + " 'team_id': 'away',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-J01DVC',\n", + " 'team_id': 'home',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-J01D1W',\n", + " 'team_id': 'home',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-J01B8N',\n", + " 'team_id': 'away',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'referee:DFL-OBJ-00011J',\n", + " 'team_id': None,\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-0002BO',\n", + " 'team_id': 'home',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-0002HE',\n", + " 'team_id': 'home',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-J01N65',\n", + " 'team_id': 'away',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-002GKO',\n", + " 'team_id': 'home',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-0000RP',\n", + " 'team_id': 'away',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'team:away',\n", + " 'team_id': 'away',\n", + " 'name': 'FC Bayern München',\n", + " 'id': 'DFL-CLU-00000G',\n", + " 'jersey_colour': '#25282A'},\n", + " {'entity_id': 'DFL-OBJ-0027B6',\n", + " 'team_id': 'home',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-0026ZI',\n", + " 'team_id': 'home',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-0027G6',\n", + " 'team_id': 'away',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-0002AU',\n", + " 'team_id': 'away',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'referee:DFL-OBJ-000141',\n", + " 'team_id': None,\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-0002DR',\n", + " 'team_id': 'away',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-J01APO',\n", + " 'team_id': 'away',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-0026PM',\n", + " 'team_id': 'away',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-002GBW',\n", + " 'team_id': 'home',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-000270',\n", + " 'team_id': 'home',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'team:home',\n", + " 'team_id': 'home',\n", + " 'name': '1. FC Köln',\n", + " 'id': 'DFL-CLU-000008',\n", + " 'jersey_colour': '#F4F5F0'},\n", + " {'entity_id': 'ball',\n", + " 'team_id': None,\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-002G89',\n", + " 'team_id': 'home',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-002G4A',\n", + " 'team_id': 'home',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'referee:DFL-OBJ-00011W',\n", + " 'team_id': None,\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-0027V2',\n", + " 'team_id': 'home',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-0027KL',\n", + " 'team_id': 'away',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-000191',\n", + " 'team_id': 'away',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-0027AX',\n", + " 'team_id': 'home',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-0002F5',\n", + " 'team_id': 'away',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-00012X',\n", + " 'team_id': 'home',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-0000IA',\n", + " 'team_id': 'away',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None},\n", + " {'entity_id': 'DFL-OBJ-0027G0',\n", + " 'team_id': 'away',\n", + " 'name': None,\n", + " 'id': None,\n", + " 'jersey_colour': None}]}" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "metadata = catalog.get_metadata(match_id=target_match)\n", "metadata" @@ -253,9 +511,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "FileNotFoundError", + "evalue": "No such file or directory (os error 2): ...rehouse/tracking/tracking_data/data/00000-0-2ce0e3ca-73c0-404e-9596-10570ff4a509.parquet (set POLARS_VERBOSE=1 to see full path)\n\nThis error occurred with the following context stack:\n\t[1] 'parquet scan'\n\t[2] 'sink'\n", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mFileNotFoundError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[8]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m start = time.perf_counter()\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m df = \u001b[43mpl\u001b[49m\u001b[43m.\u001b[49m\u001b[43mscan_parquet\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43midsse_warehouse/tracking/tracking_data/data/00000-0-2ce0e3ca-73c0-404e-9596-10570ff4a509.parquet\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcollect\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 4\u001b[39m query_time = time.perf_counter() - start\n\u001b[32m 5\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33m ✅ Loaded and pivoted \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mlen\u001b[39m(df)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m rows in \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mquery_time\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m.4f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33ms\u001b[39m\u001b[33m\"\u001b[39m)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/.venv/lib/python3.11/site-packages/polars/_utils/deprecation.py:97\u001b[39m, in \u001b[36mdeprecate_streaming_parameter..decorate..wrapper\u001b[39m\u001b[34m(*args, **kwargs)\u001b[39m\n\u001b[32m 93\u001b[39m kwargs[\u001b[33m\"\u001b[39m\u001b[33mengine\u001b[39m\u001b[33m\"\u001b[39m] = \u001b[33m\"\u001b[39m\u001b[33min-memory\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 95\u001b[39m \u001b[38;5;28;01mdel\u001b[39;00m kwargs[\u001b[33m\"\u001b[39m\u001b[33mstreaming\u001b[39m\u001b[33m\"\u001b[39m]\n\u001b[32m---> \u001b[39m\u001b[32m97\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunction\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/.venv/lib/python3.11/site-packages/polars/lazyframe/opt_flags.py:328\u001b[39m, in \u001b[36mforward_old_opt_flags..decorate..wrapper\u001b[39m\u001b[34m(*args, **kwargs)\u001b[39m\n\u001b[32m 325\u001b[39m optflags = cb(optflags, kwargs.pop(key)) \u001b[38;5;66;03m# type: ignore[no-untyped-call,unused-ignore]\u001b[39;00m\n\u001b[32m 327\u001b[39m kwargs[\u001b[33m\"\u001b[39m\u001b[33moptimizations\u001b[39m\u001b[33m\"\u001b[39m] = optflags\n\u001b[32m--> \u001b[39m\u001b[32m328\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunction\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/.venv/lib/python3.11/site-packages/polars/lazyframe/frame.py:2429\u001b[39m, in \u001b[36mLazyFrame.collect\u001b[39m\u001b[34m(self, type_coercion, predicate_pushdown, projection_pushdown, simplify_expression, slice_pushdown, comm_subplan_elim, comm_subexpr_elim, cluster_with_columns, collapse_joins, no_optimization, engine, background, optimizations, **_kwargs)\u001b[39m\n\u001b[32m 2427\u001b[39m \u001b[38;5;66;03m# Only for testing purposes\u001b[39;00m\n\u001b[32m 2428\u001b[39m callback = _kwargs.get(\u001b[33m\"\u001b[39m\u001b[33mpost_opt_callback\u001b[39m\u001b[33m\"\u001b[39m, callback)\n\u001b[32m-> \u001b[39m\u001b[32m2429\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m wrap_df(\u001b[43mldf\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcollect\u001b[49m\u001b[43m(\u001b[49m\u001b[43mengine\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcallback\u001b[49m\u001b[43m)\u001b[49m)\n", + "\u001b[31mFileNotFoundError\u001b[39m: No such file or directory (os error 2): ...rehouse/tracking/tracking_data/data/00000-0-2ce0e3ca-73c0-404e-9596-10570ff4a509.parquet (set POLARS_VERBOSE=1 to see full path)\n\nThis error occurred with the following context stack:\n\t[1] 'parquet scan'\n\t[2] 'sink'\n" + ] + } + ], "source": [ "start = time.perf_counter()\n", "\n", diff --git a/examples/idsse_warehouse/catalog.db b/examples/idsse_warehouse/catalog.db deleted file mode 100644 index 146187d1168342f190f8d595e162fea0c295b762..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20480 zcmeI&U2oD*7zgl{!RE-edZ8h{rpAkngsz40HoZ`sBpPvZ?8WS=Ih2R3QYf}%ZkHpw z@*~Y}vJ2yl-@!`{Wh(;(Hxo^k{F}D)Y#-s7~;mKINC<>SlA7zhpM{f?09RwCIVds#`8OuF9(q6<(qtW6yKdm$Xk-w?3nuKdSq4 zZo_VR%xv=Obif)di(J*Lx-RLTm_vJf%1yxo>R@B)`CMG()e%iovMI_@n$k+Hn$0We zG$oZv&RA9qmZki1%BZFC*~Rl`7C*Rl^JrbZpB{=Cs33IT|BKQ$fv;E~009U<00Izz00bZa0SG_< z0uZ>S0uM!A0=RQ~s02U-NQ`#=JlfO3#I6|JKR^|(ltqz zrN@dQNzyRA2kG5B0@MgLJM<}^YWHdmOY@(vkB5Iq!Leg$q7wcxc06&rS1`^)00Izz z00bZa0SG_<0uVT#K=skU@MJbCAId(@mxOJJyl^+oGT#y2F1Lia>pHu_^=;wxyB1u? zWiu;TWhL`uF{|{8RAxh?7Wa9{Y0RR$&EyuIWpcAI!-yzL%PY##>f+*@5*lsg&^zQ; zatG$NI5lH&iqYr={>b0Bc6RW3e7p25 zsNX8n4(uJXBYa+3cTxzJ=>{x<{4u9l%}0c{f?be+9#+y4+BG! z%aOm8r#{4M7o^c2pl52N_FRom6r{k|i8OXH@#%ce8Ad?>0uX=z1Rwwb2tWV=5V({A zRaYLFnx3hySJw-o$SWn^ce%A~mp7ZAW^HL#v`gYaG#Xv?gzH6VS)8Z617U6XUi7Kh z6Q$b5L9}CpX*p$|+hsaW;kEJRNA8L(S_Fz#WUEo1B-&gYMrpaOYAhj)R4SI&S;2_w zOwaQe%g56e)Af8ICF1G0QQ-5kxEf0^J*6fXw~Cx4bv0>d z^aavdvNKJ(6Yft8cAlPVszsei8$klVv^Cl!?n1U zpyCue^9)gO0^uVmd=&nADW6y*1px>^00Izz00bZa0SG_<0ucDG1?~;feu><7vm|hr zVrE9WVmkJ%#>r3)f0O8i0|F3$00bZa0SG_<0uX=z1Rwx`D=KhDrY(S*H@da}#J=tM yp6^yf?e~8<{9U3K4hTR10uX=z1Rwwb2tWV=5P$##uCl7la6vtZxWuetbDWzd?os>43S27=key}1@609O=(E!bJxNqjZcIdpB;odtX zg*1l5pb3c!l?rud`q~st3=2295feWGY&6D&I}r=DF&aNMgoWqc>CBsXEu}3lX`1)u z&HbNqf9K<#`%2+NeZ%9-8YU%tZ{rz-j|D{ORT!tC#g&{=bcky+rFZa*;@C>BmQy@j z5^t0+w}wr4mP@U1ydn5b$;r^61YQRfGMd8{eF9n1<-0{5!2^6u*5(OhsU<9)@ zF78Ygmk7r;=GZ&|8V`0qpHm_{9Rh%w0}jOF4uGK@k`6o|Dts9SB191#Zn7+^ua>P4gc#?ra+xnk#=h(>l@mP5bTJ7g~#R^wH_=^szfF?UR?LTgt85KfKuT`_|T`trOD|e{Jf1bkn)d %i3pbBXe)(Ph)z_|m zKl4@HE%l0erT*Ec-mAZ1AFrcFE*_yz^fX?+aC6|F7xg}U;QWP88bA8@X MUELqo`m1;UKUBG&q5uE@ diff --git a/profile_warehouse/tracking/match_metadata/metadata/snap-9142401099081597578-0-3967bf9b-f807-48db-aae8-3a65e2db3451.avro b/profile_warehouse/tracking/match_metadata/metadata/snap-9142401099081597578-0-3967bf9b-f807-48db-aae8-3a65e2db3451.avro deleted file mode 100644 index c13dddee74417b7ab28df8948d06b0b6a4104517..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1819 zcmb7ETSyd97_JSXhp>xg1SZ>HS6rLf9NpDI4e5q#45iTR83 zO$4H51QVaCtP2XI{!(7xgF!wZh{1BPJWvz`21LZige^jGO)E5EEDn(jeJHMnpy3e; z!r0IO@wGw2R59}S^FW({OJuA-IY)tE4G@^0elq0<)WNNl?Hox)Ay?t$3@Bqm;flRn zScRHm(zm8|4go#=s{^Elp-H4DAdzekO_+v>JWo+Jk4X#_wOAnvmM!8sECWKb@g8j0 z?MR^NNyc$! z`FtsZcX$R*swSb$mfRRv4;%~%)F7cHxDl`xI0(iaFmPgq0tB|{&K6`VaA9z_(5}FR zfc4OsLUwM=spG>lv)9B~8&4x978V!=NLm=`p>ExW%rXY(lS-g&ZSX|2`+8f`4^(8* zF-)tB-vFC2$P7UUV+{i2T%K__{h`6>3z9(#Rz8{IkgbBVX`1OLJxn035t^>0K53~n zoPdTDqNC|pXMs673~!P^lbeM#9*xe(${d+Fuv$X)i8Mq$z@<{NXt%k&wbCf^OZS|_@%XHzR6|XRpQXX*KY^k zb)O4#X}MpkdpaBUH^g#}lyuZ?U#50mdfC4Dkm&m|@BPTrijm%)i^VqvD+gXx_kMcT zaqs%``o!|Jr}Fwq;bruD{pqpCD|~N04n@|~Ts?gE#fMni@r?(1!gn?m_5I+7W5@P% t?FwGt>myD58}^nRJ=;c_v6+!MbTJA$}S>MjSr6Y57=54nH7Y)?X z1P}=I!i1zMe-IEn06`!`R8$BkP$^!>VIguU0V4?les88TyR$uP%O*|J&dztd-}n7~ z-}iQWONAxwx1>#Hh}d%$&ywQLe<};$$qbH+V%- z2^FZI$YJ=zm1Zf(v*CY@l*a{)fKd#KI!U@r4_@1YPO#odW1np-Vak_vD)3<2Hm1Q5 z9JZp;UdlskOOMnZjXiGLwla0zn5baZQf-{olj^OijvJ-gX>P}jQf*{3bBFQtq%{5f(oMPK;#^6A6%g+g}f3ad|s5q zL`+E@ugDbA?GeQk=Nq4!huQSFB^L;4$}1WW1KEOsAagSb1Zsk5T(>_pj)L6qvZ*$R z6J*LIT`0{$#;RVXP^0adotkEzl-W#9j*B95Yw$M&e?ZPSE>f-pv5%ZCL63;cldv3@ zB1Fr`#@AK1F+n6@ycC-Y|Ma*#I@ulTvRDofr@PHpT2I3;K zKM*Tq31YQ(Aw}caF9hubfM(RqKtsi}EHe|RPyw5Y07Qz!$Y8MzO{%4vPtZzJvqu)Q zG>_ZM5VDFk!IBTedI!d*YvN_Bvao}ZUx@k-? zd6$Y_YA_2aF=TqskzfX6Wpjow3aJZoVGhgTNEIcS^5Ai&x28Fu?PE?+C>9a(ftU{# z#`NJ}JgZ~Hn@p$~<3XdmX zW5q1e^v{?Qb~uY?=}qAAJ2J1pV%#9T71TPv!zPdnvC@mmY=Ot(6k~HkoP<>_8Dd3o zXd!!G68jmfvJ#o-AR&sV0zV8;1541Xn7K88XL0Jq?dfm4GWpU@B z^t!VRrNJE+G$t>`qaa1bJVIEGqEcMy#FkB}aC-p5I+aC$y&eXzx9b3VJzW6Un>~OT zUdX@;dYCDcBOMvb?Skp^XA?|c0KoJWcO6V$z>$52i0>=TCYWp)VtN35X`*VYwy6+K zcts^av~UD)hm94GOcx~YkZeNhEnLUM`48B)C~wNwdw$ z+BeMAs@*w0nWaE+1ePEx4@bkpodm-hRd9!DnPCU1nvr2q=Q13_zC3~rLq~D8dBUC% zC6s8AAQPl131nr+_BZ?QD8!_2^MK_}FU$a4(ExKT3gltmWH{AlaBEZ2*tOcLcyJW# zs;WkGb|Hr`twRX7-9QL-_km4W(2Z2gmCD-qG+4Tg+E@=HEDJICf+q}av??xn+m2sJ zzsGzvH7^ayZP>PE%%YsS+^H*`nbR<#PtC?p-@bCsh;ORzK6!Uy!MeAC4F@)%C6UJY zFC<0;R&Fcr?t9=^u;A6T6$eLO%AK*ia8LcXJGPhC*Ks|$+M{dw^xbfFZ{d3M*~BmV z7d)uv_xfeuwI})q>MriQx%p<(g%bx{1ES0JEUo^oa<#v)>D_bjQQMzuS}^wSP3J-< z6P1T9swdXuygPI0k^LWbf3$ky_SZ+e_xknO<>wdOg(`Or{jKS}-z&emw)o-qCk_=G z4)~KBYft5eYEM4;3Q|w4jr=%t-@YMzPaeAOO?_6$^bJ!N3V)4H4F6^InY9&7yEm`X zKWSXsaOIjZtMuQin<`e6P!zm9IR6cPSL4g4>lPKqo333BN+XX9bMqsM8sAJ@K63W7 zd~AO5YGHWmus?Q88aHj^!s_L{d)A{V)ivMu?!J4}j2`z-93H4G?~O+d7<90OVTZ#^UdvIXG^s&8`P8|@WcfPdn^BmWz^$A5U`)JJ%UypoeXkbXc z{Pm?di=VG9DAUH~?mGI#@p1F2YHl>(3jHp8xjJ2Ya{v+~>(Z2d=uX|LEa`(y4P_E06rsxaFz0FZUXMVq0c${{!O23t0dF diff --git a/profile_warehouse/tracking/tracking_data/metadata/snap-2502839378513990955-0-acf6aa22-f608-4e8d-af73-eee758db29a1.avro b/profile_warehouse/tracking/tracking_data/metadata/snap-2502839378513990955-0-acf6aa22-f608-4e8d-af73-eee758db29a1.avro deleted file mode 100644 index 58e72bcd90494c295b60b21d25c787d928affe75..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1829 zcmb7ETSyd97%sbLXx4)lOtWQ-B-*vvU8mjEhzbo9qtK)W%i-+I+1<&xHFHK?Weo$n zXuX6!ln>HHqKAZ_2MHpdimaDxh0mM;d*1b&x~p?yJr$R7wYUVkV=hZtt33N$Dq4-u9l8=_dQ!?OmIIp~oMDGaqN zZ&^gqB!E1fP}2oP&hiz2PED!e6drQ0? zT~tJXBCq4Owq8mGQu0> zwYkC7C7_t=NQ*0}52=LPwS(SBUP47wu*L#3HVFx)4y&t>V{v9yDOQ#~0x!sD zA)HfWgiT%y@&=qhRs+~n18iW10NXQ`1e|NSh08JQb7TNK4|&#_i=}GzuL2(tM5wcz zV%W>4s|mc#GuSQY2sfK`Vjw-RG4N1?2$$eQKw4lUNI9S>UCR{UC7W(kYl zkaawUNtM(Muoy$c5QG&)gg~}0PYRBIaB%X1q|sh8pY(CaR6$uZr|~B~Ou!HkPS;dl zu+*Z`1vNH|N7J^>UgGEwym16|Y6Vt9RhMNYj?5}pO(E+&tE|q&D^+zgc{F zQ~mXWOldB427E57Dhj-p)Ea6ALZ?NvX|Sjb);@_ G?*9PgNmTy; diff --git a/python/tests/test_catalog.py b/python/tests/test_catalog.py index 1eba3bb..7f3f1d9 100644 --- a/python/tests/test_catalog.py +++ b/python/tests/test_catalog.py @@ -8,12 +8,13 @@ def test_catalog_lifecycle(tmp_path): # Create a container with all required fields for Iceberg # and some constant attributes (jersey_color, player_name) + # Including a NULL value in x to verify it's allowed data = { "match/id": ["match_1", "match_1"], "timestamp": [1000000, 1040000], # microseconds "frame_id": [100, 101], "period": ["1", "1"], - "ball/x": [0.0, 1.0], + "ball/x": [0.0, None], "ball/y": [0.0, 1.0], "teams/home/players/p1/x": [10.0, 11.0], "teams/home/players/p1/y": [20.0, 21.0], @@ -49,6 +50,7 @@ def test_catalog_lifecycle(tmp_path): assert df["match/id"][0] == "match_1" assert "ball/player/id" in df.columns assert df["ball/x"][0] == 0.0 + assert df["ball/x"][1] is None # Check normalized constants assert df["teams/home/players/p1/jersey_color"][0] == "red" diff --git a/python/tracking_data_container/catalog.py b/python/tracking_data_container/catalog.py index 760dfc9..94f1b9d 100644 --- a/python/tracking_data_container/catalog.py +++ b/python/tracking_data_container/catalog.py @@ -143,7 +143,7 @@ def _create_tracking_table(self) -> None: except NoSuchTableError: pass # Create new table - # Define the requested schema. x, y, z are optional. + # Define the requested schema. x, y, z are nullable (optional in Iceberg). schema = Schema( NestedField(1, "competition_id", StringType(), required=False), NestedField(2, "season_id", StringType(), required=False), @@ -366,8 +366,13 @@ def _save_tracking_data( entities_with_constants = long_df.select(["entity_id", "team_id"]).unique().to_dicts() # 3. Validation of required fields. - # x, y, z are explicitly allowed to be null. - required_fields = [ + # x and y MUST be present as columns, but their values CAN be null. + for field in ["x", "y"]: + if field not in long_df.columns: + raise ValueError(f"Required column {field} is missing from container") + + # Identity and temporal fields MUST NOT be null. + must_not_be_null = [ "match_id", "frame_id", "period", @@ -375,7 +380,7 @@ def _save_tracking_data( "entity_type", "entity_id", ] - for field in required_fields: + for field in must_not_be_null: if field not in long_df.columns: raise ValueError(f"Required field {field} is missing from container") if long_df[field].null_count() > 0: From cdf721509fe6e0c1fd6e39c5c137361325d13c40 Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Wed, 8 Apr 2026 15:45:43 +0200 Subject: [PATCH 13/21] feat: use hierarchical structure for entity metadata --- python/tracking_data_container/catalog.py | 166 +++++++++++----------- 1 file changed, 86 insertions(+), 80 deletions(-) diff --git a/python/tracking_data_container/catalog.py b/python/tracking_data_container/catalog.py index 94f1b9d..915fe08 100644 --- a/python/tracking_data_container/catalog.py +++ b/python/tracking_data_container/catalog.py @@ -237,7 +237,7 @@ def save_container( else: raise ValueError("match_id must be provided if not present in container data") - # 1. Save tracking data and get unique entities (with constant attributes) + # 1. Save tracking data and get unique entities (hierarchical) entities = self._save_tracking_data(container, match_id) # 2. Prepare metadata @@ -290,12 +290,12 @@ def _save_tracking_data( self, container: TrackingDataContainer, match_id: str, - ) -> list[dict[str, Any]]: + ) -> dict[str, Any]: """ Save tracking data to Iceberg table in entity-long format. Returns: - List of unique entities (dict with entity_id, team_id, and constants) + Hierarchical dictionary of unique entities and constants. """ if self._tracking_table is None: self._tracking_table = self._iceberg_catalog.load_table("tracking.tracking_data") @@ -324,8 +324,7 @@ def _save_tracking_data( # Ensure match_id is present and correct long_df = long_df.with_columns(pl.lit(match_id).alias("match_id")) - # --- Constant Extraction --- - # Identify attributes that are constant for each entity + # --- Constant Extraction & Hierarchical grouping --- ignore_for_constants = { "match_id", "frame_id", @@ -341,37 +340,54 @@ def _save_tracking_data( } potential_constants = [c for c in long_df.columns if c not in ignore_for_constants] - entities_with_constants = [] - if potential_constants: - agg_exprs = [pl.col(c).n_unique().alias(f"{c}_count") for c in potential_constants] - agg_vals = [pl.col(c).first().alias(f"{c}_val") for c in potential_constants] + # Group by entity and extract constants + agg_exprs = [pl.col(c).n_unique().alias(f"{c}_count") for c in potential_constants] + agg_vals = [pl.col(c).first().alias(f"{c}_val") for c in potential_constants] - stats = long_df.group_by(["entity_id", "team_id"]).agg(agg_exprs + agg_vals) + stats = long_df.group_by(["entity_id", "team_id", "entity_type"]).agg(agg_exprs + agg_vals) - columns_to_drop = [] - for c in potential_constants: - if stats[f"{c}_count"].max() == 1: - columns_to_drop.append(c) + columns_to_drop = [] + for c in potential_constants: + if stats[f"{c}_count"].max() == 1: + columns_to_drop.append(c) - entities_with_constants = ( - stats.select(["entity_id", "team_id"] + [f"{c}_val" for c in columns_to_drop]) - .rename({f"{c}_val": c for c in columns_to_drop}) - .to_dicts() + entities_flat = ( + stats.select( + ["entity_id", "team_id", "entity_type"] + [f"{c}_val" for c in columns_to_drop] ) + .rename({f"{c}_val": c for c in columns_to_drop}) + .to_dicts() + ) - if columns_to_drop: - logger.info(f"Normalizing constant attributes to metadata: {columns_to_drop}") - long_df = long_df.drop(columns_to_drop) - else: - entities_with_constants = long_df.select(["entity_id", "team_id"]).unique().to_dicts() - - # 3. Validation of required fields. - # x and y MUST be present as columns, but their values CAN be null. + # Build hierarchical structure + entities_hierarchical: dict[str, Any] = {"ball": {}, "teams": {}, "referees": {}} + for ent in entities_flat: + etype = ent.pop("entity_type") + eid = ent.pop("entity_id") + tid = ent.pop("team_id", "none") + + if etype == "ball": + entities_hierarchical["ball"] = ent + elif etype == "referee": + entities_hierarchical["referees"][eid] = ent + elif etype == "player": + if tid not in entities_hierarchical["teams"]: + entities_hierarchical["teams"][tid] = {"players": {}, "attributes": {}} + entities_hierarchical["teams"][tid]["players"][eid] = ent + elif etype == "team": + if eid not in entities_hierarchical["teams"]: + entities_hierarchical["teams"][eid] = {"players": {}, "attributes": {}} + entities_hierarchical["teams"][eid]["attributes"] = ent + + if columns_to_drop: + logger.info(f"Normalizing constant attributes to metadata: {columns_to_drop}") + long_df = long_df.drop(columns_to_drop) + + # 3. Validation for field in ["x", "y"]: if field not in long_df.columns: raise ValueError(f"Required column {field} is missing from container") - # Identity and temporal fields MUST NOT be null. must_not_be_null = [ "match_id", "frame_id", @@ -386,10 +402,8 @@ def _save_tracking_data( if long_df[field].null_count() > 0: raise ValueError(f"Required field {field} contains null values") - # 4. Convert to Arrow and handle types + # 4. Save to Iceberg arrow_table = long_df.to_arrow() - - # Define known types for Iceberg compatibility known_types = { "timestamp": pa.timestamp("us"), "frame_id": pa.int64(), @@ -401,12 +415,10 @@ def _save_tracking_data( new_columns = [] new_fields = [] for i, field in enumerate(arrow_table.schema): - target_type = None - if field.name in known_types: - target_type = known_types[field.name] - elif isinstance(field.type, pa.DictionaryType): + target_type = known_types.get(field.name) + if not target_type and isinstance(field.type, pa.DictionaryType): target_type = field.type.value_type - elif pa.types.is_null(field.type): + elif not target_type and pa.types.is_null(field.type): target_type = pa.string() if target_type is not None: @@ -418,19 +430,13 @@ def _save_tracking_data( arrow_table = pa.Table.from_arrays(new_columns, schema=pa.schema(new_fields)) - # Start transaction with self._tracking_table.transaction() as txn: - # Update schema to include any new metrics container_schema = _pyarrow_to_schema_without_ids(arrow_table.schema) txn.update_schema().union_by_name(container_schema).commit() - - # Delete existing match partition txn.delete(EqualTo(term=Reference("match_id"), value=cast(Any, match_id))) # type: ignore - - # Append long-format data txn.append(arrow_table) - return entities_with_constants + return entities_hierarchical def _save_metadata(self, metadata: dict, match_id: str) -> None: """Save metadata to Iceberg table.""" @@ -439,7 +445,6 @@ def _save_metadata(self, metadata: dict, match_id: str) -> None: assert self._metadata_table is not None - # Define schema for the Arrow table metadata_schema = pa.schema( [ pa.field("match_id", pa.string(), nullable=False), @@ -449,7 +454,6 @@ def _save_metadata(self, metadata: dict, match_id: str) -> None: ] ) - # Prepare metadata row metadata_row = pa.table( { "match_id": [match_id], @@ -460,7 +464,6 @@ def _save_metadata(self, metadata: dict, match_id: str) -> None: schema=metadata_schema, ) - # Upsert metadata with self._metadata_table.transaction() as txn: txn.delete(EqualTo(term=Reference("match_id"), value=cast(Any, match_id))) # type: ignore txn.append(metadata_row) @@ -495,13 +498,36 @@ def __init__( self._filters = filters self._cached_schema: Fields | None = None + def _get_flat_entities(self, hierarchical_entities: dict[str, Any]) -> list[dict[str, Any]]: + """Flatten hierarchical metadata back to list for easy schema iteration.""" + flat = [] + # 1. Ball + ball = hierarchical_entities.get("ball") + if ball is not None: + flat.append({"entity_id": "ball", "team_id": "none", **ball}) + + # 2. Teams & Players + for tid, team_info in hierarchical_entities.get("teams", {}).items(): + # Team itself + flat.append( + {"entity_id": f"team:{tid}", "team_id": tid, **team_info.get("attributes", {})} + ) + # Players + for pid, p_attrs in team_info.get("players", {}).items(): + flat.append({"entity_id": pid, "team_id": tid, **p_attrs}) + + # 3. Referees + for rid, r_attrs in hierarchical_entities.get("referees", {}).items(): + flat.append({"entity_id": f"referee:{rid}", "team_id": "none", **r_attrs}) + + return flat + def schema(self) -> Fields: - """Reconstruct the wide schema by inspecting entities in the match metadata.""" + """Reconstruct the wide schema by inspecting entities in the hierarchical metadata.""" if self._cached_schema: return self._cached_schema assert self._catalog is not None - if self._catalog._tracking_table is None: self._catalog._tracking_table = self._catalog._iceberg_catalog.load_table( "tracking.tracking_data" @@ -509,18 +535,16 @@ def schema(self) -> Fields: assert self._catalog._tracking_table is not None - # Load entities from metadata metadata = self._catalog.get_metadata(self._match_id) - entities = metadata.get("_entities") - - if entities is None: + entities_hier = metadata.get("_entities") + if entities_hier is None: raise ValueError(f"Metadata for match {self._match_id} is missing unique entity list.") - # Apply player filter if requested + entities = self._get_flat_entities(entities_hier) + if self._player_id: entities = [e for e in entities if str(e.get("entity_id")) == self._player_id] - # Reconstruct wide field names physical_names = [f.name for f in self._catalog._tracking_table.schema().fields] ignored = set(COLUMN_MAPPING.values()) | { "entity_type", @@ -530,10 +554,8 @@ def schema(self) -> Fields: "period_id", "entity_key", } - # Physical attributes in the tracking table attr_cols = [c for c in physical_names if c not in ignored and c != "id"] - # Base list: all core columns that are NOT entity-specific wide_names = [] for c in CORE_COLUMNS: if c in ("player/id", "team/id", "entity_key"): @@ -545,18 +567,14 @@ def schema(self) -> Fields: for row in entities: p_id = str(row.get("entity_id")) t_id = str(row.get("team_id", "none")) - prefix = TrackedEntity.from_entity_key(f"{p_id}|{t_id}").cdf_prefix() - # 1. Attributes from physical table + for attr in attr_cols: wide_names.append(f"{prefix}{attr}") - - # 2. Attributes from metadata constants for attr in row.keys(): if attr not in ("entity_id", "team_id"): wide_names.append(f"{prefix}{attr}") - # 3. Standard CDF names wide_names.append(f"{prefix}player/id") wide_names.append(f"{prefix}team/id") @@ -590,7 +608,6 @@ def slice(self, offset: int, length: int) -> CatalogLazyTrackingDataContainer: def collect(self) -> TrackingDataContainer: """Finally perform the Iceberg scan and pivot.""" assert self._catalog is not None - if self._catalog._tracking_table is None: self._catalog._tracking_table = self._catalog._iceberg_catalog.load_table( "tracking.tracking_data" @@ -607,12 +624,12 @@ def collect(self) -> TrackingDataContainer: EqualTo(term=Reference("entity_id"), value=cast(Any, self._player_id)), # type: ignore ) - # 1. Determine which long-format columns we need selected_long_fields = None deferred_select = None metadata = self._catalog.get_metadata(self._match_id) - entities_metadata = metadata.get("_entities", []) + entities_hier = metadata.get("_entities", {}) + entities_flat = self._get_flat_entities(entities_hier) if self._selection: eager_schema = self.schema() @@ -644,7 +661,6 @@ def collect(self) -> TrackingDataContainer: if field.entity: needed_ents.add(cast(str, field.entity_key())) - # Check physical schema and map names physical_names = {f.name for f in self._catalog._tracking_table.schema().fields} selected_long_fields_set = set() for f in needed_long_internal: @@ -652,7 +668,6 @@ def collect(self) -> TrackingDataContainer: if phys_name in physical_names: selected_long_fields_set.add(phys_name) - # Always include required fields for pivot for req in [ "match_id", "entity_type", @@ -669,10 +684,8 @@ def collect(self) -> TrackingDataContainer: selected_long_fields = list(selected_long_fields_set) - # 2. Add entity filter pushdown if needed_ents: entity_predicate: BooleanExpression | None = None - for k in needed_ents: p_id, t_id = k.split("|") cond: BooleanExpression = EqualTo( @@ -689,7 +702,6 @@ def collect(self) -> TrackingDataContainer: if entity_predicate: filter_expr = And(filter_expr, entity_predicate) - # 3. Perform the scan scan_kwargs: dict[str, Any] = {"row_filter": filter_expr} if selected_long_fields is not None: scan_kwargs["selected_fields"] = tuple(selected_long_fields) @@ -699,32 +711,26 @@ def collect(self) -> TrackingDataContainer: scan = self._catalog._tracking_table.scan(**scan_kwargs) arrow_table = scan.to_arrow() - # 4. Pivot to wide format using high-performance Rust implementation wide_container = TrackingDataContainer.pivot(arrow_table) wide_container._catalog = self._catalog - # 5. Inject constant attributes from metadata - if entities_metadata: - # Reconstruct prefixes for entities in metadata - entity_map = { - f"{e['entity_id']}|{e.get('team_id', 'none')}": e for e in entities_metadata - } - + # Inject constant attributes + if entities_flat: wide_names = wide_container.schema().names() - for ek, ent_data in entity_map.items(): - prefix = TrackedEntity.from_entity_key(ek).cdf_prefix() + for ent_data in entities_flat: + p_id = ent_data["entity_id"] + t_id = ent_data["team_id"] + prefix = TrackedEntity.from_entity_key(f"{p_id}|{t_id}").cdf_prefix() for attr, value in ent_data.items(): if attr in ("entity_id", "team_id"): continue wide_attr_name = f"{prefix}{attr}" if wide_attr_name not in wide_names: - # Add constant column wide_container = wide_container.add_column( wide_attr_name, [value] * wide_container.num_rows() ) - # Apply final selection/slice if deferred_select: wide_container = wide_container.select(deferred_select) if self._limit: From 8a7d7f0b1830e72d4cf62c4738a1088e81ebd51d Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Wed, 8 Apr 2026 15:51:25 +0200 Subject: [PATCH 14/21] perf: omit None attributes from hierarchical entity metadata --- examples/08-idsse-warehouse.ipynb | 262 +++++------------- examples/idsse_warehouse/catalog.db | Bin 0 -> 20480 bytes ...-b333-4c57-a371-74456e7c8d29.metadata.json | 1 + ...-5de4-4f14-8523-98060401e712.metadata.json | 1 + ...-7558-4e44-8a81-70d8f3c5ada2.metadata.json | 1 + ...-df9e-4b34-9eab-b38255059e3d.metadata.json | 1 + ...0f7d00-f8c4-41cb-b0b8-3148f44c2ab1-m0.avro | Bin 0 -> 4508 bytes ...e4f0c2-e0d0-47d7-91b8-84d9f37863fb-m0.avro | Bin 0 -> 4509 bytes ...7521bf-42dc-4f0b-9994-9661a5bb2874-m0.avro | Bin 0 -> 4509 bytes ...-697521bf-42dc-4f0b-9994-9661a5bb2874.avro | Bin 0 -> 1947 bytes ...-120f7d00-f8c4-41cb-b0b8-3148f44c2ab1.avro | Bin 0 -> 1902 bytes ...-58e4f0c2-e0d0-47d7-91b8-84d9f37863fb.avro | Bin 0 -> 1829 bytes ...-fac7-49b0-8d04-59619f80a697.metadata.json | 1 + ...-f7bb-4910-bfe3-0320322d0420.metadata.json | 1 + ...-56f0-4692-87ea-a5971864c0b3.metadata.json | 1 + ...-3bff-4c99-b498-68fe18cf6f3b.metadata.json | 1 + ...b66841-7b73-41dd-8673-9f99990d0add-m0.avro | Bin 0 -> 6113 bytes ...0818c9-0eca-413c-a97f-b38c039de85a-m0.avro | Bin 0 -> 6118 bytes ...2b2ab0-1620-4401-8fa7-8b8077d79eab-m0.avro | Bin 0 -> 5588 bytes ...-240818c9-0eca-413c-a97f-b38c039de85a.avro | Bin 0 -> 1853 bytes ...-bc2b2ab0-1620-4401-8fa7-8b8077d79eab.avro | Bin 0 -> 2000 bytes ...-1fb66841-7b73-41dd-8673-9f99990d0add.avro | Bin 0 -> 1936 bytes python/tracking_data_container/catalog.py | 13 +- 23 files changed, 79 insertions(+), 204 deletions(-) create mode 100644 examples/idsse_warehouse/catalog.db create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00000-8ece4efe-b333-4c57-a371-74456e7c8d29.metadata.json create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00001-39d25845-5de4-4f14-8523-98060401e712.metadata.json create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00002-5ae3d21a-7558-4e44-8a81-70d8f3c5ada2.metadata.json create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00003-d74574f9-df9e-4b34-9eab-b38255059e3d.metadata.json create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/120f7d00-f8c4-41cb-b0b8-3148f44c2ab1-m0.avro create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/58e4f0c2-e0d0-47d7-91b8-84d9f37863fb-m0.avro create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/697521bf-42dc-4f0b-9994-9661a5bb2874-m0.avro create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/snap-7183120188202263762-0-697521bf-42dc-4f0b-9994-9661a5bb2874.avro create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/snap-7688096930835682381-0-120f7d00-f8c4-41cb-b0b8-3148f44c2ab1.avro create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/snap-8301423118246886670-0-58e4f0c2-e0d0-47d7-91b8-84d9f37863fb.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00000-ce626e1b-fac7-49b0-8d04-59619f80a697.metadata.json create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00001-9f918efb-f7bb-4910-bfe3-0320322d0420.metadata.json create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00002-d900c1db-56f0-4692-87ea-a5971864c0b3.metadata.json create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00003-e183f978-3bff-4c99-b498-68fe18cf6f3b.metadata.json create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/1fb66841-7b73-41dd-8673-9f99990d0add-m0.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/240818c9-0eca-413c-a97f-b38c039de85a-m0.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/bc2b2ab0-1620-4401-8fa7-8b8077d79eab-m0.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4743469712033335616-0-240818c9-0eca-413c-a97f-b38c039de85a.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-8580464386215457325-0-bc2b2ab0-1620-4401-8fa7-8b8077d79eab.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-8653266967438724888-0-1fb66841-7b73-41dd-8673-9f99990d0add.avro diff --git a/examples/08-idsse-warehouse.ipynb b/examples/08-idsse-warehouse.ipynb index ae28a5d..32bccdc 100644 --- a/examples/08-idsse-warehouse.ipynb +++ b/examples/08-idsse-warehouse.ipynb @@ -138,15 +138,15 @@ "text": [ "\n", "Processing match DFL-MAT-J03WMX...\n", - " ✅ Parsed in 20.25s, Saved in 14.32s\n", + " ✅ Parsed in 14.13s, Saved in 12.57s\n", " ✅ Rows: 145,967\n", "\n", "Processing match DFL-MAT-J03WN1...\n", - " ✅ Parsed in 19.48s, Saved in 13.57s\n", + " ✅ Parsed in 14.03s, Saved in 11.98s\n", " ✅ Rows: 141,561\n", "\n", "Processing match DFL-MAT-J03WOH...\n", - " ✅ Parsed in 13.43s, Saved in 10.51s\n", + " ✅ Parsed in 12.23s, Saved in 10.73s\n", " ✅ Rows: 137,214\n" ] } @@ -242,7 +242,7 @@ "output_type": "stream", "text": [ "Loading data for DFL-MAT-J03WMX...\n", - " ✅ Loaded and pivoted 165259 rows in 8.6644s\n" + " ✅ Loaded and pivoted 165259 rows in 9.5119s\n" ] }, { @@ -255,15 +255,15 @@ " white-space: pre-wrap;\n", "}\n", "\n", - "shape: (5, 424)
competition/idseason/idmatch/idframe_idperiodtimestampteams/away/players/DFL-OBJ-J01N65/player/idteams/away/players/DFL-OBJ-J01N65/team/idteams/away/players/DFL-OBJ-J01N65/xteams/away/players/DFL-OBJ-J01N65/yteams/away/players/DFL-OBJ-J01N65/zteams/away/players/DFL-OBJ-J01N65/velteams/away/players/DFL-OBJ-J01N65/accteams/away/players/DFL-OBJ-J01N65/entity_keyteams/home/players/DFL-OBJ-0002BO/player/idteams/home/players/DFL-OBJ-0002BO/team/idteams/home/players/DFL-OBJ-0002BO/xteams/home/players/DFL-OBJ-0002BO/yteams/home/players/DFL-OBJ-0002BO/zteams/home/players/DFL-OBJ-0002BO/velteams/home/players/DFL-OBJ-0002BO/accteams/home/players/DFL-OBJ-0002BO/entity_keyteams/home/players/DFL-OBJ-0027AX/player/idteams/home/players/DFL-OBJ-0027AX/team/idteams/home/players/DFL-OBJ-0027AX/xteams/home/players/DFL-OBJ-0027AX/yteams/home/players/DFL-OBJ-0027AX/zteams/home/players/DFL-OBJ-0027AX/velteams/home/players/DFL-OBJ-0027AX/accteams/home/players/DFL-OBJ-0027AX/entity_keyteams/home/players/DFL-OBJ-J01DVC/player/idteams/home/players/DFL-OBJ-J01DVC/team/idteams/home/players/DFL-OBJ-J01DVC/xteams/home/players/DFL-OBJ-J01DVC/yteams/home/players/DFL-OBJ-J01DVC/zteams/home/players/DFL-OBJ-J01DVC/velteams/home/players/DFL-OBJ-J01DVC/accteams/home/jersey_colourball/nameball/idball/jersey_colourteams/home/players/DFL-OBJ-002G89/nameteams/home/players/DFL-OBJ-002G89/idteams/home/players/DFL-OBJ-002G89/jersey_colourteams/home/players/DFL-OBJ-002G4A/nameteams/home/players/DFL-OBJ-002G4A/idteams/home/players/DFL-OBJ-002G4A/jersey_colourreferees/DFL-OBJ-00011W/namereferees/DFL-OBJ-00011W/idreferees/DFL-OBJ-00011W/jersey_colourteams/home/players/DFL-OBJ-0027V2/nameteams/home/players/DFL-OBJ-0027V2/idteams/home/players/DFL-OBJ-0027V2/jersey_colourteams/away/players/DFL-OBJ-0027KL/nameteams/away/players/DFL-OBJ-0027KL/idteams/away/players/DFL-OBJ-0027KL/jersey_colourteams/away/players/DFL-OBJ-000191/nameteams/away/players/DFL-OBJ-000191/idteams/away/players/DFL-OBJ-000191/jersey_colourteams/home/players/DFL-OBJ-0027AX/nameteams/home/players/DFL-OBJ-0027AX/idteams/home/players/DFL-OBJ-0027AX/jersey_colourteams/away/players/DFL-OBJ-0002F5/nameteams/away/players/DFL-OBJ-0002F5/idteams/away/players/DFL-OBJ-0002F5/jersey_colourteams/home/players/DFL-OBJ-00012X/nameteams/home/players/DFL-OBJ-00012X/idteams/home/players/DFL-OBJ-00012X/jersey_colourteams/away/players/DFL-OBJ-0000IA/nameteams/away/players/DFL-OBJ-0000IA/idteams/away/players/DFL-OBJ-0000IA/jersey_colourteams/away/players/DFL-OBJ-0027G0/nameteams/away/players/DFL-OBJ-0027G0/idteams/away/players/DFL-OBJ-0027G0/jersey_colour
strstrstri64strdatetime[μs]strstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64strnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnull
nullnull"DFL-MAT-J03WMX"10000"first_half"2023-05-27 13:30:12.600nullnullnullnullnullnullnullnull"DFL-OBJ-0002BO""home"17.2620.85null0.00.0"DFL-OBJ-0002BO|home""DFL-OBJ-0027AX""home"6.95.12null0.00.0"DFL-OBJ-0027AX|home"nullnullnullnullnullnullnull"#F4F5F0"nullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnull
nullnull"DFL-MAT-J03WMX"10001"first_half"2023-05-27 13:30:12.640nullnullnullnullnullnullnullnull"DFL-OBJ-0002BO""home"17.2320.879999null4.431.08"DFL-OBJ-0002BO|home""DFL-OBJ-0027AX""home"6.825.1null8.943.24"DFL-OBJ-0027AX|home"nullnullnullnullnullnullnull"#F4F5F0"nullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnull
nullnull"DFL-MAT-J03WMX"10002"first_half"2023-05-27 13:30:12.680nullnullnullnullnullnullnullnull"DFL-OBJ-0002BO""home"17.1820.91null4.61.07"DFL-OBJ-0002BO|home""DFL-OBJ-0027AX""home"6.725.08null9.393.06"DFL-OBJ-0027AX|home"nullnullnullnullnullnullnull"#F4F5F0"nullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnull
nullnull"DFL-MAT-J03WMX"10003"first_half"2023-05-27 13:30:12.720nullnullnullnullnullnullnullnull"DFL-OBJ-0002BO""home"17.1520.940001null4.741.03"DFL-OBJ-0002BO|home""DFL-OBJ-0027AX""home"6.625.05null9.822.84"DFL-OBJ-0027AX|home"nullnullnullnullnullnullnull"#F4F5F0"nullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnull
nullnull"DFL-MAT-J03WMX"10004"first_half"2023-05-27 13:30:12.760nullnullnullnullnullnullnullnull"DFL-OBJ-0002BO""home"17.11000120.98null4.880.98"DFL-OBJ-0002BO|home""DFL-OBJ-0027AX""home"6.55.02null10.212.6"DFL-OBJ-0027AX|home"nullnullnullnullnullnullnull"#F4F5F0"nullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnull
" + "shape: (5, 424)
competition/idseason/idmatch/idframe_idperiodtimestampteams/home/players/DFL-OBJ-J01D1W/player/idteams/home/players/DFL-OBJ-J01D1W/team/idteams/home/players/DFL-OBJ-J01D1W/xteams/home/players/DFL-OBJ-J01D1W/yteams/home/players/DFL-OBJ-J01D1W/zteams/home/players/DFL-OBJ-J01D1W/accteams/home/players/DFL-OBJ-J01D1W/velteams/home/players/DFL-OBJ-J01D1W/entity_keyteams/home/player/idteams/home/team/idteams/home/xteams/home/yteams/home/zteams/home/accteams/home/velteams/home/entity_keyteams/home/players/DFL-OBJ-00012X/player/idteams/home/players/DFL-OBJ-00012X/team/idteams/home/players/DFL-OBJ-00012X/xteams/home/players/DFL-OBJ-00012X/yteams/home/players/DFL-OBJ-00012X/zteams/home/players/DFL-OBJ-00012X/accteams/home/players/DFL-OBJ-00012X/velteams/home/players/DFL-OBJ-00012X/entity_keyteams/home/players/DFL-OBJ-0026ZI/player/idteams/home/players/DFL-OBJ-0026ZI/team/idteams/home/players/DFL-OBJ-0026ZI/xteams/home/players/DFL-OBJ-0026ZI/yteams/home/players/DFL-OBJ-0026ZI/zteams/home/players/DFL-OBJ-0026ZI/accteams/home/players/DFL-OBJ-0026ZI/velteams/away/players/DFL-OBJ-0027G6/nameteams/away/players/DFL-OBJ-J01B8N/jersey_colourteams/away/players/DFL-OBJ-J01B8N/idteams/away/players/DFL-OBJ-J01B8N/nameteams/away/players/DFL-OBJ-0002DR/jersey_colourteams/away/players/DFL-OBJ-0002DR/idteams/away/players/DFL-OBJ-0002DR/nameteams/away/players/DFL-OBJ-0026PM/jersey_colourteams/away/players/DFL-OBJ-0026PM/idteams/away/players/DFL-OBJ-0026PM/nameteams/away/players/DFL-OBJ-0002AU/jersey_colourteams/away/players/DFL-OBJ-0002AU/idteams/away/players/DFL-OBJ-0002AU/nameteams/away/players/DFL-OBJ-J01N65/jersey_colourteams/away/players/DFL-OBJ-J01N65/idteams/away/players/DFL-OBJ-J01N65/nameteams/away/players/DFL-OBJ-J01APO/jersey_colourteams/away/players/DFL-OBJ-J01APO/idteams/away/players/DFL-OBJ-J01APO/nameteams/away/players/DFL-OBJ-0027KL/jersey_colourteams/away/players/DFL-OBJ-0027KL/idteams/away/players/DFL-OBJ-0027KL/nameteams/away/players/DFL-OBJ-000191/jersey_colourteams/away/players/DFL-OBJ-000191/idteams/away/players/DFL-OBJ-000191/nameteams/team:away/jersey_colourteams/team:away/idteams/team:away/namereferees/referee:DFL-OBJ-00011J/jersey_colourreferees/referee:DFL-OBJ-00011J/idreferees/referee:DFL-OBJ-00011J/namereferees/referee:DFL-OBJ-000141/jersey_colourreferees/referee:DFL-OBJ-000141/idreferees/referee:DFL-OBJ-000141/namereferees/referee:DFL-OBJ-00011W/jersey_colourreferees/referee:DFL-OBJ-00011W/idreferees/referee:DFL-OBJ-00011W/name
strstrstri64strdatetime[μs]strstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64nullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullstrstrstrnullnullnullnullnullnullnullnullnull
nullnull"DFL-MAT-J03WMX"10000"first_half"2023-05-27 13:30:12.600"DFL-OBJ-J01D1W""home"18.84-4.49null0.00.0"DFL-OBJ-J01D1W|home""team:home""home"nullnullnullnullnull"team:home|home""DFL-OBJ-00012X""home"13.08-13.71null0.00.0"DFL-OBJ-00012X|home"nullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnull"#25282A""DFL-CLU-00000G""FC Bayern München"nullnullnullnullnullnullnullnullnull
nullnull"DFL-MAT-J03WMX"10001"first_half"2023-05-27 13:30:12.640"DFL-OBJ-J01D1W""home"18.790001-4.49null0.574.08"DFL-OBJ-J01D1W|home""team:home""home"nullnullnullnullnull"team:home|home""DFL-OBJ-00012X""home"13.06-13.75null1.43.49"DFL-OBJ-00012X|home"nullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnull"#25282A""DFL-CLU-00000G""FC Bayern München"nullnullnullnullnullnullnullnullnull
nullnull"DFL-MAT-J03WMX"10002"first_half"2023-05-27 13:30:12.680"DFL-OBJ-J01D1W""home"18.75-4.49null0.84.19"DFL-OBJ-J01D1W|home""team:home""home"nullnullnullnullnull"team:home|home""DFL-OBJ-00012X""home"13.05-13.78null1.343.73"DFL-OBJ-00012X|home"nullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnull"#25282A""DFL-CLU-00000G""FC Bayern München"nullnullnullnullnullnullnullnullnull
nullnull"DFL-MAT-J03WMX"10003"first_half"2023-05-27 13:30:12.720"DFL-OBJ-J01D1W""home"18.700001-4.48null1.074.33"DFL-OBJ-J01D1W|home""team:home""home"nullnullnullnullnull"team:home|home""DFL-OBJ-00012X""home"13.03-13.82null1.243.95"DFL-OBJ-00012X|home"nullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnull"#25282A""DFL-CLU-00000G""FC Bayern München"nullnullnullnullnullnullnullnullnull
nullnull"DFL-MAT-J03WMX"10004"first_half"2023-05-27 13:30:12.760"DFL-OBJ-J01D1W""home"18.65-4.48null1.334.5"DFL-OBJ-J01D1W|home""team:home""home"nullnullnullnullnull"team:home|home""DFL-OBJ-00012X""home"13.02-13.87null1.114.13"DFL-OBJ-00012X|home"nullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnull"#25282A""DFL-CLU-00000G""FC Bayern München"nullnullnullnullnullnullnullnullnull
" ], "text/plain": [ "shape: (5, 424)\n", "┌───────────┬───────────┬───────────┬──────────┬───┬───────────┬───────────┬───────────┬───────────┐\n", - "│ competiti ┆ season/id ┆ match/id ┆ frame_id ┆ … ┆ teams/awa ┆ teams/awa ┆ teams/awa ┆ teams/awa │\n", - "│ on/id ┆ --- ┆ --- ┆ --- ┆ ┆ y/players ┆ y/players ┆ y/players ┆ y/players │\n", - "│ --- ┆ str ┆ str ┆ i64 ┆ ┆ /DFL-OBJ- ┆ /DFL-OBJ- ┆ /DFL-OBJ- ┆ /DFL-OBJ- │\n", - "│ str ┆ ┆ ┆ ┆ ┆ 000… ┆ 002… ┆ 002… ┆ 002… │\n", + "│ competiti ┆ season/id ┆ match/id ┆ frame_id ┆ … ┆ referees/ ┆ referees/ ┆ referees/ ┆ referees/ │\n", + "│ on/id ┆ --- ┆ --- ┆ --- ┆ ┆ referee:D ┆ referee:D ┆ referee:D ┆ referee:D │\n", + "│ --- ┆ str ┆ str ┆ i64 ┆ ┆ FL-OBJ-00 ┆ FL-OBJ-00 ┆ FL-OBJ-00 ┆ FL-OBJ-00 │\n", + "│ str ┆ ┆ ┆ ┆ ┆ 014… ┆ 011… ┆ 011… ┆ 011… │\n", "│ ┆ ┆ ┆ ┆ ┆ --- ┆ --- ┆ --- ┆ --- │\n", "│ ┆ ┆ ┆ ┆ ┆ null ┆ null ┆ null ┆ null │\n", "╞═══════════╪═══════════╪═══════════╪══════════╪═══╪═══════════╪═══════════╪═══════════╪═══════════╡\n", @@ -307,196 +307,60 @@ { "data": { "text/plain": [ - "{'_entities': [{'entity_id': 'DFL-OBJ-002GCR',\n", - " 'team_id': 'away',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-0028BD',\n", - " 'team_id': 'home',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-J015S4',\n", - " 'team_id': 'away',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-J01BGM',\n", - " 'team_id': 'home',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-J017RE',\n", - " 'team_id': 'away',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-J01DVC',\n", - " 'team_id': 'home',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-J01D1W',\n", - " 'team_id': 'home',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-J01B8N',\n", - " 'team_id': 'away',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'referee:DFL-OBJ-00011J',\n", - " 'team_id': None,\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-0002BO',\n", - " 'team_id': 'home',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-0002HE',\n", - " 'team_id': 'home',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-J01N65',\n", - " 'team_id': 'away',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-002GKO',\n", - " 'team_id': 'home',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-0000RP',\n", - " 'team_id': 'away',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'team:away',\n", - " 'team_id': 'away',\n", - " 'name': 'FC Bayern München',\n", - " 'id': 'DFL-CLU-00000G',\n", - " 'jersey_colour': '#25282A'},\n", - " {'entity_id': 'DFL-OBJ-0027B6',\n", - " 'team_id': 'home',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-0026ZI',\n", - " 'team_id': 'home',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-0027G6',\n", - " 'team_id': 'away',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-0002AU',\n", - " 'team_id': 'away',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'referee:DFL-OBJ-000141',\n", - " 'team_id': None,\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-0002DR',\n", - " 'team_id': 'away',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-J01APO',\n", - " 'team_id': 'away',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-0026PM',\n", - " 'team_id': 'away',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-002GBW',\n", - " 'team_id': 'home',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-000270',\n", - " 'team_id': 'home',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'team:home',\n", - " 'team_id': 'home',\n", - " 'name': '1. FC Köln',\n", - " 'id': 'DFL-CLU-000008',\n", - " 'jersey_colour': '#F4F5F0'},\n", - " {'entity_id': 'ball',\n", - " 'team_id': None,\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-002G89',\n", - " 'team_id': 'home',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-002G4A',\n", - " 'team_id': 'home',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'referee:DFL-OBJ-00011W',\n", - " 'team_id': None,\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-0027V2',\n", - " 'team_id': 'home',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-0027KL',\n", - " 'team_id': 'away',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-000191',\n", - " 'team_id': 'away',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-0027AX',\n", - " 'team_id': 'home',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-0002F5',\n", - " 'team_id': 'away',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-00012X',\n", - " 'team_id': 'home',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-0000IA',\n", - " 'team_id': 'away',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None},\n", - " {'entity_id': 'DFL-OBJ-0027G0',\n", - " 'team_id': 'away',\n", - " 'name': None,\n", - " 'id': None,\n", - " 'jersey_colour': None}]}" + "{'_entities': {'ball': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'teams': {'home': {'players': {'DFL-OBJ-000270': {'jersey_colour': None,\n", + " 'id': None,\n", + " 'name': None},\n", + " 'DFL-OBJ-002GKO': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-00012X': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-J01BGM': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-J01D1W': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-002GBW': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-0026ZI': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-0027AX': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-J01DVC': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-0002HE': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-0027B6': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-0002BO': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-0027V2': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-002G89': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-0028BD': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-002G4A': {'jersey_colour': None, 'id': None, 'name': None}},\n", + " 'attributes': {}},\n", + " 'team:home': {'players': {},\n", + " 'attributes': {'jersey_colour': '#F4F5F0',\n", + " 'id': 'DFL-CLU-000008',\n", + " 'name': '1. FC Köln'}},\n", + " 'away': {'players': {'DFL-OBJ-J015S4': {'jersey_colour': None,\n", + " 'id': None,\n", + " 'name': None},\n", + " 'DFL-OBJ-0000RP': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-0000IA': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-0027G0': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-002GCR': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-J017RE': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-0002F5': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-0027G6': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-J01B8N': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-0002DR': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-0026PM': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-0002AU': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-J01N65': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-J01APO': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-0027KL': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'DFL-OBJ-000191': {'jersey_colour': None, 'id': None, 'name': None}},\n", + " 'attributes': {}},\n", + " 'team:away': {'players': {},\n", + " 'attributes': {'jersey_colour': '#25282A',\n", + " 'id': 'DFL-CLU-00000G',\n", + " 'name': 'FC Bayern München'}}},\n", + " 'referees': {'referee:DFL-OBJ-00011J': {'jersey_colour': None,\n", + " 'id': None,\n", + " 'name': None},\n", + " 'referee:DFL-OBJ-000141': {'jersey_colour': None, 'id': None, 'name': None},\n", + " 'referee:DFL-OBJ-00011W': {'jersey_colour': None,\n", + " 'id': None,\n", + " 'name': None}}}}" ] }, "execution_count": 7, diff --git a/examples/idsse_warehouse/catalog.db b/examples/idsse_warehouse/catalog.db new file mode 100644 index 0000000000000000000000000000000000000000..2eaef264c4ebc4efd8b456427f6919d31cf0fa25 GIT binary patch literal 20480 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 literal 0 HcmV?d00001 diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/697521bf-42dc-4f0b-9994-9661a5bb2874-m0.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/697521bf-42dc-4f0b-9994-9661a5bb2874-m0.avro new file mode 100644 index 0000000000000000000000000000000000000000..755b059e089acb8f51c3d539ed0ea826392c8d1b GIT binary patch literal 4509 zcmb`KU1%It6vu0*STv**ZSf=Ebx6_#Htl@mYg253#AwvU(pHR#>u`7GZZqlZ%yi~X zLac!*1qDNGQ=!&`Ce$KDN^8M@ppYPf5vt%)U4??L_QfY(1Rp&2PG)vylcq_!5W?== zx&L$KcRud9uVzOcsHyg}VUoxHZ9O5-afb+ybjVCX`ploBkQ zXHVoY)rM7AOU3pW)=~KGlHtHu{t!B?5+SZ|r zkX(T$tHj!z?E<{!QQ~?)ph|uWkGl3A&DM!lqm!J8DXGzHi-MJH%+Fo?_rQnK`@#?* zz^vIW@JCUQ5JV{m4}u6{>U+_z+<7Fz!$E|RU|3X`NTJvc3p?JhO?ga~MT2cpY1@!s z`KHNaO&fxpfe+v__Dw2uBKA{K5;B2U8c{7jpCxXYH_B#ZbB0M&Z%yr%&1&<^U{G~gHTDldIswqKxN#Z|ro-ii z#CW)&qIq>jwqCI{%t{cwcBl`H8FU!+9!5ONNHQg4f06pl{(3|#AL&N3~ zezYMtxg2341MA7g#TJ4pJb)b*qJR!EbVP0VV?a4e$PeA{jwQpz9SRa+CCW5tFFRTR zlxaXIixHrWqfBocr7Tt&C9jGcpz-)s2}$y*|K9rT??=jYjpPySe6)I zdGml}X%m3u$^mA)5a$Is%sN!U9Sd7)!c^KTnXO6(z*N$khpBWVykA%0E9pwYWRnZ) z2auNm)u?L+9U_U>w8x06W`R3Qg+MaikmS}%qLtfNp0-CuHrxp0wzbi&dg{pSm1E0U zA#}+tK*ErMcq{9~(g)h4LtC+Sz0XJ;yYtYUgqup(_eOiOq76`O*Xt4Hv{DdAJ6J zPP54>oHv5eYRz-}Fmr+8EDS-RABlvA3j~HI#^J)V(!+I@YMF;^8}qQ2-L7H6@Q}|Y zPna`wLW!FfUFhq zz8;uW9hZ3Q(hJA52PWVu0w&l+1tw*~X z`sIe-L_9!#IRE;kgWvvB_1;!3J1GDA_nkkU?!Vgg^U~s?bGoTn+5PKFs@nBc$NZIn zg}KJArl$GE*7RqW2mY*|Pqpq|K=UsyTyB)o`;@)U?ytX5C+$ynU9Xdr?Tr@)&wes= z=G@@b*$ao>oOx2cIPmeOXVt-(hl_Kw(*sj8J7%W`r?wTo*xpdx-CgW1-Ynd{wIntj zD1P5q|3#=xwO^ z{He**PIG7NHCu6=Or{JY1$UU=yJ=jIQ+ gGxAnnt}i!oV`}G_7bXQVhtZ=?Yihw10P_aivR!s literal 0 HcmV?d00001 diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-7183120188202263762-0-697521bf-42dc-4f0b-9994-9661a5bb2874.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-7183120188202263762-0-697521bf-42dc-4f0b-9994-9661a5bb2874.avro new file mode 100644 index 0000000000000000000000000000000000000000..57f50f6ffc2e7fa112e316b340c0d11a5916dfae GIT binary patch literal 1947 zcmb7EYe*DP7|m^aOY}nwN^6?zL1oPBy6aexR1ny^q!&w#cW3VAZAnp)R1n=+oo#eg3J0jaGpY`&ED{1)z@kAi#z3Ad)``^DqoSnLIkyBz2P! zA|k;cUV;RGNjB8#Re4mYgbS&K`BY3F(%~6Y9~v@LH}#>yT2y32GG;3XCJgbPT;#Pi-DHaii>YyjWY02|n20oyZHB$8`)3;Sd6CS(Aj2u0o; zlfG(p{{ucC$x!D76tb7kWLfYw&)^nG$E4YOA_l7mHU<&O5R(!T5m+s-5p+4Aq0)W| zV69EJw;*0Y0*1sEniV7DWaO{F~qgf-L?&A=z zf;MS3kWX@$KuE?UT|>RcQp>0mYP_F}rfr>B%h9p$mSL#Ve_;&;gZ;8vj?6!>8baoY z)I1DFhr@r-E`nve0x=K)1~#iKM*pXH{5~6-M282gyY*qnPA>YsXz0P|(NTFL2cK}> zy;@@}pPR6Cw7m=-v_o`=eTO ziz>xq`3Iz{lgj#5`LGbN1o;BLb%uUu(=i bQ;x z9L~%v#mv%W!HWu-27Gx^O!jRoUvw|!HRuAndWad_%JU*0V_8LefB1t&n@4^C212F@GJ(P`ekzPg? zKZyv+#s>e5&c5Ep^aD}R@fapmQa8Y4^jU@=psErCihX%faQuUVlNTh7rj2~k$01V% zY0@;}Pkfj_L_#=SLw&+hOKKEqTmX-zZJlY$(XsHx5Y)+kutvh+X<02tW(KT=ka;3C z55CpVT_X@0s=t5W|hV0{}j*MXJeDdcuwSFZXar5^R?wlncY5BP*hEWnK<@-a)oLYTkLgDnYgno-5x_d(1IdGW_9T&*j_9k!?c-FRSZ^ z+KRV?pyLU*`E7AWzi{>-P>^soUyW*&4js+_&9%De=N_bMBWP z&h8UO*FC%QX6)Mq7+m8P9ZgQTQT=gHaba%=C literal 0 HcmV?d00001 diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-8301423118246886670-0-58e4f0c2-e0d0-47d7-91b8-84d9f37863fb.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-8301423118246886670-0-58e4f0c2-e0d0-47d7-91b8-84d9f37863fb.avro new file mode 100644 index 0000000000000000000000000000000000000000..98c58a19be3abe896ef3cb377e88d565739a89b8 GIT binary patch literal 1829 zcmb7ETSyd97~WQhip_d3yID>{nUEVZ>+ZS=lAD=Uf}shL#c+1!tUEO`XPr5#t+o|N z1@$C7<%1MNL5L*L4SI;cB7zEi2=bxJQ|KERUikw;H zDKsOqwk?G0+~^7$m4bOiFMfpe?WwWE{{@_Ywv0bkprE$W`FL;B28) zfdc{Up*@A%+!_nVhofe%oi#U}M9eKL&~=bBF~mdFybsxB43Q@pL)F~ixoGG0Hm4uR znnA`et&+I`7Gotf1QCQ32sHchWZ>k71g9@Z7R{UaWRF9x3f7`IO+LwC0tp3^bWQbn zORb=8s0$G?nznW3siQ;jCNVVFWmpsO_>!#Dky!z&DP)~UJ-~(7RB9RRvoI;B5CaKd zV6%!c`ai{U_u1SeHs!kFJ!OpC%F{z;awXZ%?w_vuDU<F66zu{b@I*i_ef{=$yp>G8k6ibnfp|G4C~g9~&2 E2Oz{(bpQYW literal 0 HcmV?d00001 diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-ce626e1b-fac7-49b0-8d04-59619f80a697.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-ce626e1b-fac7-49b0-8d04-59619f80a697.metadata.json new file mode 100644 index 0000000..a7ed8d4 --- /dev/null +++ b/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-ce626e1b-fac7-49b0-8d04-59619f80a697.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"ec386c35-f77c-47e0-aa2b-65b7405bcec2","last-updated-ms":1775656012053,"last-column-id":12,"schemas":[{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":true},{"id":4,"name":"frame_id","type":"long","required":true},{"id":5,"name":"period","type":"string","required":true},{"id":6,"name":"timestamp","type":"timestamp","required":true},{"id":7,"name":"entity_type","type":"string","required":true},{"id":8,"name":"entity_id","type":"string","required":true},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[{"source-id":3,"field-id":1000,"transform":"identity","name":"match_id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"snapshots":[],"snapshot-log":[],"metadata-log":[],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":0} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-9f918efb-f7bb-4910-bfe3-0320322d0420.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-9f918efb-f7bb-4910-bfe3-0320322d0420.metadata.json new file mode 100644 index 0000000..8884c79 --- /dev/null +++ b/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-9f918efb-f7bb-4910-bfe3-0320322d0420.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"ec386c35-f77c-47e0-aa2b-65b7405bcec2","last-updated-ms":1775656038745,"last-column-id":15,"schemas":[{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":true},{"id":4,"name":"frame_id","type":"long","required":true},{"id":5,"name":"period","type":"string","required":true},{"id":6,"name":"timestamp","type":"timestamp","required":true},{"id":7,"name":"entity_type","type":"string","required":true},{"id":8,"name":"entity_id","type":"string","required":true},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":false},{"id":4,"name":"frame_id","type":"long","required":false},{"id":5,"name":"period","type":"string","required":false},{"id":6,"name":"timestamp","type":"timestamp","required":false},{"id":7,"name":"entity_type","type":"string","required":false},{"id":8,"name":"entity_id","type":"string","required":false},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false},{"id":13,"name":"acc","type":"double","required":false},{"id":14,"name":"vel","type":"double","required":false},{"id":15,"name":"entity_key","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":3,"field-id":1000,"transform":"identity","name":"match_id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":4743469712033335616,"snapshots":[{"snapshot-id":4743469712033335616,"sequence-number":1,"timestamp-ms":1775656038745,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4743469712033335616-0-240818c9-0eca-413c-a97f-b38c039de85a.avro","summary":{"operation":"append","added-files-size":"41544688","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"41544688","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":4743469712033335616,"timestamp-ms":1775656038745}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-ce626e1b-fac7-49b0-8d04-59619f80a697.metadata.json","timestamp-ms":1775656012053}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":4743469712033335616,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":1} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-d900c1db-56f0-4692-87ea-a5971864c0b3.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-d900c1db-56f0-4692-87ea-a5971864c0b3.metadata.json new file mode 100644 index 0000000..83158e8 --- /dev/null +++ b/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-d900c1db-56f0-4692-87ea-a5971864c0b3.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"ec386c35-f77c-47e0-aa2b-65b7405bcec2","last-updated-ms":1775656064756,"last-column-id":15,"schemas":[{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":true},{"id":4,"name":"frame_id","type":"long","required":true},{"id":5,"name":"period","type":"string","required":true},{"id":6,"name":"timestamp","type":"timestamp","required":true},{"id":7,"name":"entity_type","type":"string","required":true},{"id":8,"name":"entity_id","type":"string","required":true},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":false},{"id":4,"name":"frame_id","type":"long","required":false},{"id":5,"name":"period","type":"string","required":false},{"id":6,"name":"timestamp","type":"timestamp","required":false},{"id":7,"name":"entity_type","type":"string","required":false},{"id":8,"name":"entity_id","type":"string","required":false},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false},{"id":13,"name":"acc","type":"double","required":false},{"id":14,"name":"vel","type":"double","required":false},{"id":15,"name":"entity_key","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":3,"field-id":1000,"transform":"identity","name":"match_id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":8653266967438724888,"snapshots":[{"snapshot-id":4743469712033335616,"sequence-number":1,"timestamp-ms":1775656038745,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4743469712033335616-0-240818c9-0eca-413c-a97f-b38c039de85a.avro","summary":{"operation":"append","added-files-size":"41544688","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"41544688","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":8653266967438724888,"parent-snapshot-id":4743469712033335616,"sequence-number":2,"timestamp-ms":1775656064756,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-8653266967438724888-0-1fb66841-7b73-41dd-8673-9f99990d0add.avro","summary":{"operation":"append","added-files-size":"39668903","added-data-files":"2","added-records":"3832763","changed-partition-count":"1","total-data-files":"4","total-delete-files":"0","total-records":"7919839","total-files-size":"81213591","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":4743469712033335616,"timestamp-ms":1775656038745},{"snapshot-id":8653266967438724888,"timestamp-ms":1775656064756}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-ce626e1b-fac7-49b0-8d04-59619f80a697.metadata.json","timestamp-ms":1775656012053},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-9f918efb-f7bb-4910-bfe3-0320322d0420.metadata.json","timestamp-ms":1775656038745}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":8653266967438724888,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":2} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-e183f978-3bff-4c99-b498-68fe18cf6f3b.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-e183f978-3bff-4c99-b498-68fe18cf6f3b.metadata.json new file mode 100644 index 0000000..fde2cde --- /dev/null +++ b/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-e183f978-3bff-4c99-b498-68fe18cf6f3b.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"ec386c35-f77c-47e0-aa2b-65b7405bcec2","last-updated-ms":1775656087713,"last-column-id":15,"schemas":[{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":true},{"id":4,"name":"frame_id","type":"long","required":true},{"id":5,"name":"period","type":"string","required":true},{"id":6,"name":"timestamp","type":"timestamp","required":true},{"id":7,"name":"entity_type","type":"string","required":true},{"id":8,"name":"entity_id","type":"string","required":true},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":false},{"id":4,"name":"frame_id","type":"long","required":false},{"id":5,"name":"period","type":"string","required":false},{"id":6,"name":"timestamp","type":"timestamp","required":false},{"id":7,"name":"entity_type","type":"string","required":false},{"id":8,"name":"entity_id","type":"string","required":false},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false},{"id":13,"name":"acc","type":"double","required":false},{"id":14,"name":"vel","type":"double","required":false},{"id":15,"name":"entity_key","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":3,"field-id":1000,"transform":"identity","name":"match_id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":8580464386215457325,"snapshots":[{"snapshot-id":4743469712033335616,"sequence-number":1,"timestamp-ms":1775656038745,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4743469712033335616-0-240818c9-0eca-413c-a97f-b38c039de85a.avro","summary":{"operation":"append","added-files-size":"41544688","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"41544688","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":8653266967438724888,"parent-snapshot-id":4743469712033335616,"sequence-number":2,"timestamp-ms":1775656064756,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-8653266967438724888-0-1fb66841-7b73-41dd-8673-9f99990d0add.avro","summary":{"operation":"append","added-files-size":"39668903","added-data-files":"2","added-records":"3832763","changed-partition-count":"1","total-data-files":"4","total-delete-files":"0","total-records":"7919839","total-files-size":"81213591","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":8580464386215457325,"parent-snapshot-id":8653266967438724888,"sequence-number":3,"timestamp-ms":1775656087713,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-8580464386215457325-0-bc2b2ab0-1620-4401-8fa7-8b8077d79eab.avro","summary":{"operation":"append","added-files-size":"35456426","added-data-files":"1","added-records":"3430350","changed-partition-count":"1","total-data-files":"5","total-delete-files":"0","total-records":"11350189","total-files-size":"116670017","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":4743469712033335616,"timestamp-ms":1775656038745},{"snapshot-id":8653266967438724888,"timestamp-ms":1775656064756},{"snapshot-id":8580464386215457325,"timestamp-ms":1775656087713}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-ce626e1b-fac7-49b0-8d04-59619f80a697.metadata.json","timestamp-ms":1775656012053},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-9f918efb-f7bb-4910-bfe3-0320322d0420.metadata.json","timestamp-ms":1775656038745},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-d900c1db-56f0-4692-87ea-a5971864c0b3.metadata.json","timestamp-ms":1775656064756}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":8580464386215457325,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":3} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/1fb66841-7b73-41dd-8673-9f99990d0add-m0.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/1fb66841-7b73-41dd-8673-9f99990d0add-m0.avro new file mode 100644 index 0000000000000000000000000000000000000000..a95de04e7e7f41414cd89b03a215582138523fa2 GIT binary patch literal 6113 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 literal 0 HcmV?d00001 diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-8653266967438724888-0-1fb66841-7b73-41dd-8673-9f99990d0add.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-8653266967438724888-0-1fb66841-7b73-41dd-8673-9f99990d0add.avro new file mode 100644 index 0000000000000000000000000000000000000000..ae6acff1e2406f55df225bb6883ec8a4dddcef0e GIT binary patch literal 1936 zcmb7ETSyd97|sy#L5fIhqp)GfF0@;_v$wGX?M1Xq%L;QN#8H$XH_r8heI#O@~o~ zkY#XYdMSFAlmX96XeOMMC4@bn4RUz1f%FD2Qw=bI-3G8ZW0A+Xrdv24gC{Ek;5f)J z`k2gBvvUFX053q5Vdapye7nnlH+cq+^D4s4X0kCDJuoqFP=E-Rkd44-fr((s0Yz?| zrvQhs>E;$>D#*f+-9o*BECfam%_(H&R-HXQcEju)BBhWZ;E3sk1&RXNGz|7o)XqhE z8EO2BM^Mx@cse@!dTY}UcuB=$m{OU#0Xk!eVF>)PEI=TcmuCu&UvO~hf~3(7EuZvp z$W%e7pcK1?7aAe^qIK4Yl`xfLpmACIPKogIdwW8iH=P$mDt8VUyIWi=d`1+Z#D z`iWHBcA9K&|A+Qb*v5zu0S+KwvdUoee~M@Bv$jcO`=V3JPZr%9&)KtVN0^B61C6<7 za<`8}dSg9DuHL-4*4O9TKg8^bBq9-CB;L2TZsEEd;b?40LGAU*G25dm+ne#~ON%%gLnWQW7?<`QCK;SyLlTm3O!($1VGv%Db=JmAh+lCtuJF z-WM-RcY8-C$hX52RPpN(*YeK~oj-{mE^9yfH4$D_AK%eY`t#F@ma@r;f#0hvi{uB< H*}nV*f_8|~ literal 0 HcmV?d00001 diff --git a/python/tracking_data_container/catalog.py b/python/tracking_data_container/catalog.py index 915fe08..d898c0a 100644 --- a/python/tracking_data_container/catalog.py +++ b/python/tracking_data_container/catalog.py @@ -359,25 +359,28 @@ def _save_tracking_data( .to_dicts() ) - # Build hierarchical structure + # Build hierarchical structure, omitting None attributes entities_hierarchical: dict[str, Any] = {"ball": {}, "teams": {}, "referees": {}} for ent in entities_flat: etype = ent.pop("entity_type") eid = ent.pop("entity_id") tid = ent.pop("team_id", "none") + # Remove attributes that are None for this specific entity + attrs = {k: v for k, v in ent.items() if v is not None} + if etype == "ball": - entities_hierarchical["ball"] = ent + entities_hierarchical["ball"] = attrs elif etype == "referee": - entities_hierarchical["referees"][eid] = ent + entities_hierarchical["referees"][eid] = attrs elif etype == "player": if tid not in entities_hierarchical["teams"]: entities_hierarchical["teams"][tid] = {"players": {}, "attributes": {}} - entities_hierarchical["teams"][tid]["players"][eid] = ent + entities_hierarchical["teams"][tid]["players"][eid] = attrs elif etype == "team": if eid not in entities_hierarchical["teams"]: entities_hierarchical["teams"][eid] = {"players": {}, "attributes": {}} - entities_hierarchical["teams"][eid]["attributes"] = ent + entities_hierarchical["teams"][eid]["attributes"] = attrs if columns_to_drop: logger.info(f"Normalizing constant attributes to metadata: {columns_to_drop}") From f4e9b4b0ed6b443e90bc177d54f0c36e730df556 Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Wed, 8 Apr 2026 15:56:48 +0200 Subject: [PATCH 15/21] perf: merge team-level attributes into hierarchical team metadata --- examples/08-idsse-warehouse.ipynb | 138 +++++++++--------- examples/idsse_warehouse/catalog.db | Bin 20480 -> 20480 bytes ...501c-45f2-9996-77ebcb5463a5.metadata.json} | 2 +- ...-5de4-4f14-8523-98060401e712.metadata.json | 1 - ...-5f2a-4530-bd47-a196cd47b46e.metadata.json | 1 + ...-c402-4825-a184-fa65adc17cda.metadata.json | 1 + ...-7558-4e44-8a81-70d8f3c5ada2.metadata.json | 1 - ...-90a1-4f37-8af3-8be2f399c28b.metadata.json | 1 + ...-df9e-4b34-9eab-b38255059e3d.metadata.json | 1 - ...1226c-e1c9-4c2b-8a42-09812bdcba85-m0.avro} | Bin 4509 -> 4507 bytes ...c7ea8-2fa8-4a02-9d11-6aca1be81ea1-m0.avro} | Bin 4508 -> 4507 bytes ...16f68-89e7-49e5-8303-1c604053a6e7-m0.avro} | Bin 4509 -> 4508 bytes ...fb416f68-89e7-49e5-8303-1c604053a6e7.avro} | Bin 1902 -> 1902 bytes ...d85c7ea8-2fa8-4a02-9d11-6aca1be81ea1.avro} | Bin 1947 -> 1948 bytes ...7e31226c-e1c9-4c2b-8a42-09812bdcba85.avro} | Bin 1829 -> 1826 bytes ...fb8e-4c6e-9d72-b475830bc2bb.metadata.json} | 2 +- ...a178-4260-a16a-3dc465003124.metadata.json} | 2 +- ...8c50-41ea-b5cc-28d517cd4616.metadata.json} | 2 +- ...-47d1-4eb3-8895-324002808f47.metadata.json | 1 + ...-3bff-4c99-b498-68fe18cf6f3b.metadata.json | 1 - ...dfc8e-3ec5-4fb9-b6c2-5b5b776d94bb-m0.avro} | Bin 6113 -> 6113 bytes ...e70eb-6eb2-4a19-9b08-5b01b851d469-m0.avro} | Bin 5588 -> 5589 bytes ...a80a6-f0eb-4290-8070-14be9d80f7e7-m0.avro} | Bin 6118 -> 6122 bytes ...fc9a80a6-f0eb-4290-8070-14be9d80f7e7.avro} | Bin 1853 -> 1852 bytes ...ecfdfc8e-3ec5-4fb9-b6c2-5b5b776d94bb.avro} | Bin 1936 -> 1939 bytes ...-f58e70eb-6eb2-4a19-9b08-5b01b851d469.avro | Bin 0 -> 2001 bytes ...-bc2b2ab0-1620-4401-8fa7-8b8077d79eab.avro | Bin 2000 -> 0 bytes python/tracking_data_container/catalog.py | 8 +- 28 files changed, 78 insertions(+), 84 deletions(-) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{00000-8ece4efe-b333-4c57-a371-74456e7c8d29.metadata.json => 00000-b6d20000-501c-45f2-9996-77ebcb5463a5.metadata.json} (85%) delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00001-39d25845-5de4-4f14-8523-98060401e712.metadata.json create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00001-cb7462b7-5f2a-4530-bd47-a196cd47b46e.metadata.json create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00002-570bd3e4-c402-4825-a184-fa65adc17cda.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00002-5ae3d21a-7558-4e44-8a81-70d8f3c5ada2.metadata.json create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00003-50271329-90a1-4f37-8af3-8be2f399c28b.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00003-d74574f9-df9e-4b34-9eab-b38255059e3d.metadata.json rename examples/idsse_warehouse/tracking/match_metadata/metadata/{58e4f0c2-e0d0-47d7-91b8-84d9f37863fb-m0.avro => 7e31226c-e1c9-4c2b-8a42-09812bdcba85-m0.avro} (92%) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{120f7d00-f8c4-41cb-b0b8-3148f44c2ab1-m0.avro => d85c7ea8-2fa8-4a02-9d11-6aca1be81ea1-m0.avro} (93%) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{697521bf-42dc-4f0b-9994-9661a5bb2874-m0.avro => fb416f68-89e7-49e5-8303-1c604053a6e7-m0.avro} (92%) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{snap-7688096930835682381-0-120f7d00-f8c4-41cb-b0b8-3148f44c2ab1.avro => snap-4686198960413679258-0-fb416f68-89e7-49e5-8303-1c604053a6e7.avro} (77%) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{snap-7183120188202263762-0-697521bf-42dc-4f0b-9994-9661a5bb2874.avro => snap-5701652792860281111-0-d85c7ea8-2fa8-4a02-9d11-6aca1be81ea1.avro} (75%) rename examples/idsse_warehouse/tracking/match_metadata/metadata/{snap-8301423118246886670-0-58e4f0c2-e0d0-47d7-91b8-84d9f37863fb.avro => snap-8842646200136284049-0-7e31226c-e1c9-4c2b-8a42-09812bdcba85.avro} (83%) rename examples/idsse_warehouse/tracking/tracking_data/metadata/{00000-ce626e1b-fac7-49b0-8d04-59619f80a697.metadata.json => 00000-3899460b-fb8e-4c6e-9d72-b475830bc2bb.metadata.json} (91%) rename examples/idsse_warehouse/tracking/tracking_data/metadata/{00001-9f918efb-f7bb-4910-bfe3-0320322d0420.metadata.json => 00001-0203e49b-a178-4260-a16a-3dc465003124.metadata.json} (71%) rename examples/idsse_warehouse/tracking/tracking_data/metadata/{00002-d900c1db-56f0-4692-87ea-a5971864c0b3.metadata.json => 00002-ec4a76bd-8c50-41ea-b5cc-28d517cd4616.metadata.json} (65%) create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00003-1ebc3653-47d1-4eb3-8895-324002808f47.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00003-e183f978-3bff-4c99-b498-68fe18cf6f3b.metadata.json rename examples/idsse_warehouse/tracking/tracking_data/metadata/{1fb66841-7b73-41dd-8673-9f99990d0add-m0.avro => ecfdfc8e-3ec5-4fb9-b6c2-5b5b776d94bb-m0.avro} (81%) rename examples/idsse_warehouse/tracking/tracking_data/metadata/{bc2b2ab0-1620-4401-8fa7-8b8077d79eab-m0.avro => f58e70eb-6eb2-4a19-9b08-5b01b851d469-m0.avro} (89%) rename examples/idsse_warehouse/tracking/tracking_data/metadata/{240818c9-0eca-413c-a97f-b38c039de85a-m0.avro => fc9a80a6-f0eb-4290-8070-14be9d80f7e7-m0.avro} (81%) rename examples/idsse_warehouse/tracking/tracking_data/metadata/{snap-4743469712033335616-0-240818c9-0eca-413c-a97f-b38c039de85a.avro => snap-1282166195388124131-0-fc9a80a6-f0eb-4290-8070-14be9d80f7e7.avro} (86%) rename examples/idsse_warehouse/tracking/tracking_data/metadata/{snap-8653266967438724888-0-1fb66841-7b73-41dd-8673-9f99990d0add.avro => snap-5997955711447099734-0-ecfdfc8e-3ec5-4fb9-b6c2-5b5b776d94bb.avro} (75%) create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-601469116504892914-0-f58e70eb-6eb2-4a19-9b08-5b01b851d469.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-8580464386215457325-0-bc2b2ab0-1620-4401-8fa7-8b8077d79eab.avro diff --git a/examples/08-idsse-warehouse.ipynb b/examples/08-idsse-warehouse.ipynb index 32bccdc..e8e7ed8 100644 --- a/examples/08-idsse-warehouse.ipynb +++ b/examples/08-idsse-warehouse.ipynb @@ -138,15 +138,15 @@ "text": [ "\n", "Processing match DFL-MAT-J03WMX...\n", - " ✅ Parsed in 14.13s, Saved in 12.57s\n", + " ✅ Parsed in 1.54s, Saved in 4.82s\n", " ✅ Rows: 145,967\n", "\n", "Processing match DFL-MAT-J03WN1...\n", - " ✅ Parsed in 14.03s, Saved in 11.98s\n", + " ✅ Parsed in 1.38s, Saved in 4.65s\n", " ✅ Rows: 141,561\n", "\n", "Processing match DFL-MAT-J03WOH...\n", - " ✅ Parsed in 12.23s, Saved in 10.73s\n", + " ✅ Parsed in 1.28s, Saved in 4.18s\n", " ✅ Rows: 137,214\n" ] } @@ -242,7 +242,7 @@ "output_type": "stream", "text": [ "Loading data for DFL-MAT-J03WMX...\n", - " ✅ Loaded and pivoted 165259 rows in 9.5119s\n" + " ✅ Loaded and pivoted 165259 rows in 1.4731s\n" ] }, { @@ -255,28 +255,28 @@ " white-space: pre-wrap;\n", "}\n", "\n", - "shape: (5, 424)
competition/idseason/idmatch/idframe_idperiodtimestampteams/home/players/DFL-OBJ-J01D1W/player/idteams/home/players/DFL-OBJ-J01D1W/team/idteams/home/players/DFL-OBJ-J01D1W/xteams/home/players/DFL-OBJ-J01D1W/yteams/home/players/DFL-OBJ-J01D1W/zteams/home/players/DFL-OBJ-J01D1W/accteams/home/players/DFL-OBJ-J01D1W/velteams/home/players/DFL-OBJ-J01D1W/entity_keyteams/home/player/idteams/home/team/idteams/home/xteams/home/yteams/home/zteams/home/accteams/home/velteams/home/entity_keyteams/home/players/DFL-OBJ-00012X/player/idteams/home/players/DFL-OBJ-00012X/team/idteams/home/players/DFL-OBJ-00012X/xteams/home/players/DFL-OBJ-00012X/yteams/home/players/DFL-OBJ-00012X/zteams/home/players/DFL-OBJ-00012X/accteams/home/players/DFL-OBJ-00012X/velteams/home/players/DFL-OBJ-00012X/entity_keyteams/home/players/DFL-OBJ-0026ZI/player/idteams/home/players/DFL-OBJ-0026ZI/team/idteams/home/players/DFL-OBJ-0026ZI/xteams/home/players/DFL-OBJ-0026ZI/yteams/home/players/DFL-OBJ-0026ZI/zteams/home/players/DFL-OBJ-0026ZI/accteams/home/players/DFL-OBJ-0026ZI/velteams/away/players/DFL-OBJ-0027G6/nameteams/away/players/DFL-OBJ-J01B8N/jersey_colourteams/away/players/DFL-OBJ-J01B8N/idteams/away/players/DFL-OBJ-J01B8N/nameteams/away/players/DFL-OBJ-0002DR/jersey_colourteams/away/players/DFL-OBJ-0002DR/idteams/away/players/DFL-OBJ-0002DR/nameteams/away/players/DFL-OBJ-0026PM/jersey_colourteams/away/players/DFL-OBJ-0026PM/idteams/away/players/DFL-OBJ-0026PM/nameteams/away/players/DFL-OBJ-0002AU/jersey_colourteams/away/players/DFL-OBJ-0002AU/idteams/away/players/DFL-OBJ-0002AU/nameteams/away/players/DFL-OBJ-J01N65/jersey_colourteams/away/players/DFL-OBJ-J01N65/idteams/away/players/DFL-OBJ-J01N65/nameteams/away/players/DFL-OBJ-J01APO/jersey_colourteams/away/players/DFL-OBJ-J01APO/idteams/away/players/DFL-OBJ-J01APO/nameteams/away/players/DFL-OBJ-0027KL/jersey_colourteams/away/players/DFL-OBJ-0027KL/idteams/away/players/DFL-OBJ-0027KL/nameteams/away/players/DFL-OBJ-000191/jersey_colourteams/away/players/DFL-OBJ-000191/idteams/away/players/DFL-OBJ-000191/nameteams/team:away/jersey_colourteams/team:away/idteams/team:away/namereferees/referee:DFL-OBJ-00011J/jersey_colourreferees/referee:DFL-OBJ-00011J/idreferees/referee:DFL-OBJ-00011J/namereferees/referee:DFL-OBJ-000141/jersey_colourreferees/referee:DFL-OBJ-000141/idreferees/referee:DFL-OBJ-000141/namereferees/referee:DFL-OBJ-00011W/jersey_colourreferees/referee:DFL-OBJ-00011W/idreferees/referee:DFL-OBJ-00011W/name
strstrstri64strdatetime[μs]strstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64nullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullstrstrstrnullnullnullnullnullnullnullnullnull
nullnull"DFL-MAT-J03WMX"10000"first_half"2023-05-27 13:30:12.600"DFL-OBJ-J01D1W""home"18.84-4.49null0.00.0"DFL-OBJ-J01D1W|home""team:home""home"nullnullnullnullnull"team:home|home""DFL-OBJ-00012X""home"13.08-13.71null0.00.0"DFL-OBJ-00012X|home"nullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnull"#25282A""DFL-CLU-00000G""FC Bayern München"nullnullnullnullnullnullnullnullnull
nullnull"DFL-MAT-J03WMX"10001"first_half"2023-05-27 13:30:12.640"DFL-OBJ-J01D1W""home"18.790001-4.49null0.574.08"DFL-OBJ-J01D1W|home""team:home""home"nullnullnullnullnull"team:home|home""DFL-OBJ-00012X""home"13.06-13.75null1.43.49"DFL-OBJ-00012X|home"nullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnull"#25282A""DFL-CLU-00000G""FC Bayern München"nullnullnullnullnullnullnullnullnull
nullnull"DFL-MAT-J03WMX"10002"first_half"2023-05-27 13:30:12.680"DFL-OBJ-J01D1W""home"18.75-4.49null0.84.19"DFL-OBJ-J01D1W|home""team:home""home"nullnullnullnullnull"team:home|home""DFL-OBJ-00012X""home"13.05-13.78null1.343.73"DFL-OBJ-00012X|home"nullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnull"#25282A""DFL-CLU-00000G""FC Bayern München"nullnullnullnullnullnullnullnullnull
nullnull"DFL-MAT-J03WMX"10003"first_half"2023-05-27 13:30:12.720"DFL-OBJ-J01D1W""home"18.700001-4.48null1.074.33"DFL-OBJ-J01D1W|home""team:home""home"nullnullnullnullnull"team:home|home""DFL-OBJ-00012X""home"13.03-13.82null1.243.95"DFL-OBJ-00012X|home"nullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnull"#25282A""DFL-CLU-00000G""FC Bayern München"nullnullnullnullnullnullnullnullnull
nullnull"DFL-MAT-J03WMX"10004"first_half"2023-05-27 13:30:12.760"DFL-OBJ-J01D1W""home"18.65-4.48null1.334.5"DFL-OBJ-J01D1W|home""team:home""home"nullnullnullnullnull"team:home|home""DFL-OBJ-00012X""home"13.02-13.87null1.114.13"DFL-OBJ-00012X|home"nullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnullnull"#25282A""DFL-CLU-00000G""FC Bayern München"nullnullnullnullnullnullnullnullnull
" + "shape: (5, 316)
competition/idseason/idmatch/idframe_idperiodtimestampteams/away/players/DFL-OBJ-J017RE/player/idteams/away/players/DFL-OBJ-J017RE/team/idteams/away/players/DFL-OBJ-J017RE/xteams/away/players/DFL-OBJ-J017RE/yteams/away/players/DFL-OBJ-J017RE/zteams/away/players/DFL-OBJ-J017RE/accteams/away/players/DFL-OBJ-J017RE/velteams/away/players/DFL-OBJ-J017RE/entity_keyteams/away/players/DFL-OBJ-J01APO/player/idteams/away/players/DFL-OBJ-J01APO/team/idteams/away/players/DFL-OBJ-J01APO/xteams/away/players/DFL-OBJ-J01APO/yteams/away/players/DFL-OBJ-J01APO/zteams/away/players/DFL-OBJ-J01APO/accteams/away/players/DFL-OBJ-J01APO/velteams/away/players/DFL-OBJ-J01APO/entity_keyteams/home/players/DFL-OBJ-002GKO/player/idteams/home/players/DFL-OBJ-002GKO/team/idteams/home/players/DFL-OBJ-002GKO/xteams/home/players/DFL-OBJ-002GKO/yteams/home/players/DFL-OBJ-002GKO/zteams/home/players/DFL-OBJ-002GKO/accteams/home/players/DFL-OBJ-002GKO/velteams/home/players/DFL-OBJ-002GKO/entity_keyteams/away/players/DFL-OBJ-0000RP/player/idteams/away/players/DFL-OBJ-0000RP/team/idteams/away/players/DFL-OBJ-0000RP/xteams/away/players/DFL-OBJ-0000RP/yteams/away/players/DFL-OBJ-0000RP/zteams/away/players/DFL-OBJ-0000RP/accteams/away/players/DFL-OBJ-0000RP/velteams/home/players/DFL-OBJ-002GBW/team/idteams/home/players/DFL-OBJ-002GBW/xteams/home/players/DFL-OBJ-002GBW/yteams/home/players/DFL-OBJ-002GBW/zteams/home/players/DFL-OBJ-002GBW/accteams/home/players/DFL-OBJ-002GBW/velteams/home/players/DFL-OBJ-002GBW/entity_keyteams/home/players/DFL-OBJ-002G4A/player/idteams/home/players/DFL-OBJ-002G4A/team/idteams/home/players/DFL-OBJ-002G4A/xteams/home/players/DFL-OBJ-002G4A/yteams/home/players/DFL-OBJ-002G4A/zteams/home/players/DFL-OBJ-002G4A/accteams/home/players/DFL-OBJ-002G4A/velteams/home/players/DFL-OBJ-002G4A/entity_keyteams/away/players/DFL-OBJ-0027G6/player/idteams/away/players/DFL-OBJ-0027G6/team/idteams/away/players/DFL-OBJ-0027G6/xteams/away/players/DFL-OBJ-0027G6/yteams/away/players/DFL-OBJ-0027G6/zteams/away/players/DFL-OBJ-0027G6/accteams/away/players/DFL-OBJ-0027G6/velteams/away/players/DFL-OBJ-0027G6/entity_keyteams/home/players/DFL-OBJ-0027V2/player/idteams/home/players/DFL-OBJ-0027V2/team/idteams/home/players/DFL-OBJ-0027V2/xteams/home/players/DFL-OBJ-0027V2/yteams/home/players/DFL-OBJ-0027V2/zteams/home/players/DFL-OBJ-0027V2/accteams/home/players/DFL-OBJ-0027V2/velteams/home/players/DFL-OBJ-0027V2/entity_keyteams/team:home/idteams/team:home/nameteams/team:home/jersey_colourteams/team:away/idteams/team:away/nameteams/team:away/jersey_colour
strstrstri64strdatetime[μs]strstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64strf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrstrstrstrstr
nullnull"DFL-MAT-J03WMX"10000"first_half"2023-05-27 13:30:12.600"DFL-OBJ-J017RE""away"-20.779.07null0.00.0"DFL-OBJ-J017RE|away""DFL-OBJ-J01APO""away"-13.825.719999null0.00.0"DFL-OBJ-J01APO|away""DFL-OBJ-002GKO""home"11.375.18null0.00.0"DFL-OBJ-002GKO|home"nullnullnullnullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-002G4A""home"10.39-3.39null0.00.0"DFL-OBJ-002G4A|home""DFL-OBJ-0027G6""away"0.78-0.54null0.00.0"DFL-OBJ-0027G6|away"nullnullnullnullnullnullnullnull"DFL-CLU-000008""1. FC Köln""#F4F5F0""DFL-CLU-00000G""FC Bayern München""#25282A"
nullnull"DFL-MAT-J03WMX"10001"first_half"2023-05-27 13:30:12.640"DFL-OBJ-J017RE""away"-20.779.11null2.113.1"DFL-OBJ-J017RE|away""DFL-OBJ-J01APO""away"-13.7825.74null1.232.59"DFL-OBJ-J01APO|away""DFL-OBJ-002GKO""home"11.345.23null1.354.77"DFL-OBJ-002GKO|home"nullnullnullnullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-002G4A""home"10.28-3.39null1.2210.72"DFL-OBJ-002G4A|home""DFL-OBJ-0027G6""away"0.85-0.56null2.347.74"DFL-OBJ-0027G6|away"nullnullnullnullnullnullnullnull"DFL-CLU-000008""1. FC Köln""#F4F5F0""DFL-CLU-00000G""FC Bayern München""#25282A"
nullnull"DFL-MAT-J03WMX"10002"first_half"2023-05-27 13:30:12.680"DFL-OBJ-J017RE""away"-20.779.14null2.063.4"DFL-OBJ-J017RE|away""DFL-OBJ-J01APO""away"-13.7525.76null1.082.76"DFL-OBJ-J01APO|away""DFL-OBJ-002GKO""home"11.315.28null1.264.95"DFL-OBJ-002GKO|home"nullnullnullnullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-002G4A""home"10.16-3.39null1.1310.9"DFL-OBJ-002G4A|home""DFL-OBJ-0027G6""away"0.94-0.59null2.288.08"DFL-OBJ-0027G6|away"nullnullnullnullnullnullnullnull"DFL-CLU-000008""1. FC Köln""#F4F5F0""DFL-CLU-00000G""FC Bayern München""#25282A"
nullnull"DFL-MAT-J03WMX"10003"first_half"2023-05-27 13:30:12.720"DFL-OBJ-J017RE""away"-20.779.19null2.03.7"DFL-OBJ-J017RE|away""DFL-OBJ-J01APO""away"-13.7325.780001null0.912.89"DFL-OBJ-J01APO|away""DFL-OBJ-002GKO""home"11.295.33null1.135.12"DFL-OBJ-002GKO|home"nullnullnullnullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-002G4A""home"10.03-3.39null1.0311.06"DFL-OBJ-002G4A|home""DFL-OBJ-0027G6""away"1.03-0.62null2.268.4"DFL-OBJ-0027G6|away"nullnullnullnullnullnullnullnull"DFL-CLU-000008""1. FC Köln""#F4F5F0""DFL-CLU-00000G""FC Bayern München""#25282A"
nullnull"DFL-MAT-J03WMX"10004"first_half"2023-05-27 13:30:12.760"DFL-OBJ-J017RE""away"-20.7800019.22null1.923.98"DFL-OBJ-J017RE|away""DFL-OBJ-J01APO""away"-13.7125.809999null0.723.0"DFL-OBJ-J01APO|away""DFL-OBJ-002GKO""home"11.265.38null0.985.28"DFL-OBJ-002GKO|home"nullnullnullnullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-002G4A""home"9.91-3.39null0.9211.19"DFL-OBJ-002G4A|home""DFL-OBJ-0027G6""away"1.12-0.65null2.298.74"DFL-OBJ-0027G6|away"nullnullnullnullnullnullnullnull"DFL-CLU-000008""1. FC Köln""#F4F5F0""DFL-CLU-00000G""FC Bayern München""#25282A"
" ], "text/plain": [ - "shape: (5, 424)\n", + "shape: (5, 316)\n", "┌───────────┬───────────┬───────────┬──────────┬───┬───────────┬───────────┬───────────┬───────────┐\n", - "│ competiti ┆ season/id ┆ match/id ┆ frame_id ┆ … ┆ referees/ ┆ referees/ ┆ referees/ ┆ referees/ │\n", - "│ on/id ┆ --- ┆ --- ┆ --- ┆ ┆ referee:D ┆ referee:D ┆ referee:D ┆ referee:D │\n", - "│ --- ┆ str ┆ str ┆ i64 ┆ ┆ FL-OBJ-00 ┆ FL-OBJ-00 ┆ FL-OBJ-00 ┆ FL-OBJ-00 │\n", - "│ str ┆ ┆ ┆ ┆ ┆ 014… ┆ 011… ┆ 011… ┆ 011… │\n", - "│ ┆ ┆ ┆ ┆ ┆ --- ┆ --- ┆ --- ┆ --- │\n", - "│ ┆ ┆ ┆ ┆ ┆ null ┆ null ┆ null ┆ null │\n", + "│ competiti ┆ season/id ┆ match/id ┆ frame_id ┆ … ┆ teams/tea ┆ teams/tea ┆ teams/tea ┆ teams/tea │\n", + "│ on/id ┆ --- ┆ --- ┆ --- ┆ ┆ m:home/je ┆ m:away/id ┆ m:away/na ┆ m:away/je │\n", + "│ --- ┆ str ┆ str ┆ i64 ┆ ┆ rsey_colo ┆ --- ┆ me ┆ rsey_colo │\n", + "│ str ┆ ┆ ┆ ┆ ┆ ur ┆ str ┆ --- ┆ ur │\n", + "│ ┆ ┆ ┆ ┆ ┆ --- ┆ ┆ str ┆ --- │\n", + "│ ┆ ┆ ┆ ┆ ┆ str ┆ ┆ ┆ str │\n", "╞═══════════╪═══════════╪═══════════╪══════════╪═══╪═══════════╪═══════════╪═══════════╪═══════════╡\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10000 ┆ … ┆ null ┆ null ┆ null ┆ null │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ ┆ ┆ ┆ │\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10001 ┆ … ┆ null ┆ null ┆ null ┆ null │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ ┆ ┆ ┆ │\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10002 ┆ … ┆ null ┆ null ┆ null ┆ null │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ ┆ ┆ ┆ │\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10003 ┆ … ┆ null ┆ null ┆ null ┆ null │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ ┆ ┆ ┆ │\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10004 ┆ … ┆ null ┆ null ┆ null ┆ null │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ ┆ ┆ ┆ │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10000 ┆ … ┆ #F4F5F0 ┆ DFL-CLU-0 ┆ FC Bayern ┆ #25282A │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ ┆ 0000G ┆ München ┆ │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10001 ┆ … ┆ #F4F5F0 ┆ DFL-CLU-0 ┆ FC Bayern ┆ #25282A │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ ┆ 0000G ┆ München ┆ │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10002 ┆ … ┆ #F4F5F0 ┆ DFL-CLU-0 ┆ FC Bayern ┆ #25282A │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ ┆ 0000G ┆ München ┆ │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10003 ┆ … ┆ #F4F5F0 ┆ DFL-CLU-0 ┆ FC Bayern ┆ #25282A │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ ┆ 0000G ┆ München ┆ │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10004 ┆ … ┆ #F4F5F0 ┆ DFL-CLU-0 ┆ FC Bayern ┆ #25282A │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ ┆ 0000G ┆ München ┆ │\n", "└───────────┴───────────┴───────────┴──────────┴───┴───────────┴───────────┴───────────┴───────────┘" ] }, @@ -307,60 +307,52 @@ { "data": { "text/plain": [ - "{'_entities': {'ball': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'teams': {'home': {'players': {'DFL-OBJ-000270': {'jersey_colour': None,\n", - " 'id': None,\n", - " 'name': None},\n", - " 'DFL-OBJ-002GKO': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-00012X': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-J01BGM': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-J01D1W': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-002GBW': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-0026ZI': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-0027AX': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-J01DVC': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-0002HE': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-0027B6': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-0002BO': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-0027V2': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-002G89': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-0028BD': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-002G4A': {'jersey_colour': None, 'id': None, 'name': None}},\n", + "{'_entities': {'ball': {},\n", + " 'teams': {'away': {'players': {'DFL-OBJ-0000IA': {},\n", + " 'DFL-OBJ-J01APO': {},\n", + " 'DFL-OBJ-002GCR': {},\n", + " 'DFL-OBJ-0002DR': {},\n", + " 'DFL-OBJ-J01N65': {},\n", + " 'DFL-OBJ-0002F5': {},\n", + " 'DFL-OBJ-J01B8N': {},\n", + " 'DFL-OBJ-0000RP': {},\n", + " 'DFL-OBJ-0002AU': {},\n", + " 'DFL-OBJ-0027G6': {},\n", + " 'DFL-OBJ-0027KL': {},\n", + " 'DFL-OBJ-J015S4': {},\n", + " 'DFL-OBJ-000191': {},\n", + " 'DFL-OBJ-0026PM': {},\n", + " 'DFL-OBJ-0027G0': {},\n", + " 'DFL-OBJ-J017RE': {}},\n", " 'attributes': {}},\n", - " 'team:home': {'players': {},\n", - " 'attributes': {'jersey_colour': '#F4F5F0',\n", - " 'id': 'DFL-CLU-000008',\n", - " 'name': '1. FC Köln'}},\n", - " 'away': {'players': {'DFL-OBJ-J015S4': {'jersey_colour': None,\n", - " 'id': None,\n", - " 'name': None},\n", - " 'DFL-OBJ-0000RP': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-0000IA': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-0027G0': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-002GCR': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-J017RE': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-0002F5': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-0027G6': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-J01B8N': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-0002DR': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-0026PM': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-0002AU': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-J01N65': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-J01APO': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-0027KL': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'DFL-OBJ-000191': {'jersey_colour': None, 'id': None, 'name': None}},\n", + " 'home': {'players': {'DFL-OBJ-002GBW': {},\n", + " 'DFL-OBJ-J01BGM': {},\n", + " 'DFL-OBJ-002G4A': {},\n", + " 'DFL-OBJ-J01DVC': {},\n", + " 'DFL-OBJ-0002HE': {},\n", + " 'DFL-OBJ-000270': {},\n", + " 'DFL-OBJ-0027AX': {},\n", + " 'DFL-OBJ-0002BO': {},\n", + " 'DFL-OBJ-0027V2': {},\n", + " 'DFL-OBJ-J01D1W': {},\n", + " 'DFL-OBJ-0026ZI': {},\n", + " 'DFL-OBJ-0028BD': {},\n", + " 'DFL-OBJ-002GKO': {},\n", + " 'DFL-OBJ-00012X': {},\n", + " 'DFL-OBJ-0027B6': {},\n", + " 'DFL-OBJ-002G89': {}},\n", " 'attributes': {}},\n", + " 'team:home': {'players': {},\n", + " 'attributes': {'id': 'DFL-CLU-000008',\n", + " 'name': '1. FC Köln',\n", + " 'jersey_colour': '#F4F5F0'}},\n", " 'team:away': {'players': {},\n", - " 'attributes': {'jersey_colour': '#25282A',\n", - " 'id': 'DFL-CLU-00000G',\n", - " 'name': 'FC Bayern München'}}},\n", - " 'referees': {'referee:DFL-OBJ-00011J': {'jersey_colour': None,\n", - " 'id': None,\n", - " 'name': None},\n", - " 'referee:DFL-OBJ-000141': {'jersey_colour': None, 'id': None, 'name': None},\n", - " 'referee:DFL-OBJ-00011W': {'jersey_colour': None,\n", - " 'id': None,\n", - " 'name': None}}}}" + " 'attributes': {'id': 'DFL-CLU-00000G',\n", + " 'name': 'FC Bayern München',\n", + " 'jersey_colour': '#25282A'}}},\n", + " 'referees': {'referee:DFL-OBJ-000141': {},\n", + " 'referee:DFL-OBJ-00011J': {},\n", + " 'referee:DFL-OBJ-00011W': {}}}}" ] }, "execution_count": 7, diff --git a/examples/idsse_warehouse/catalog.db b/examples/idsse_warehouse/catalog.db index 2eaef264c4ebc4efd8b456427f6919d31cf0fa25..ddd8e0c4d06a3c82012485f2d19bcd89ebf9defe 100644 GIT binary patch delta 176 zcmWm6u@S;B3;@8(0&EacB(3gbS+eB~Si_d>l9D<#Z7QfTLhcxbJ(vLa0e;_bf=! zc}kEl|HkekXzstZH0nMjBi$b3bvveMj##R148};nN&?v_L(f5SwSSvmpXr2O;gc~n delta 177 zcmWN}F%H5o3_wx!0^Fc^$J?U<%pIb(?hGP3}6B9I~xa%kdMxwNu;e$yrV0kK;$ Aq5uE@ diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-8ece4efe-b333-4c57-a371-74456e7c8d29.metadata.json b/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-b6d20000-501c-45f2-9996-77ebcb5463a5.metadata.json similarity index 85% rename from examples/idsse_warehouse/tracking/match_metadata/metadata/00000-8ece4efe-b333-4c57-a371-74456e7c8d29.metadata.json rename to examples/idsse_warehouse/tracking/match_metadata/metadata/00000-b6d20000-501c-45f2-9996-77ebcb5463a5.metadata.json index 10152d3..0c7ff98 100644 --- a/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-8ece4efe-b333-4c57-a371-74456e7c8d29.metadata.json +++ b/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-b6d20000-501c-45f2-9996-77ebcb5463a5.metadata.json @@ -1 +1 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata","table-uuid":"34db1268-9881-4ad7-b8b2-e44d1fdb5614","last-updated-ms":1775656012055,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match_id","type":"string","required":true},{"id":2,"name":"competition_id","type":"string","required":false},{"id":3,"name":"season_id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"snapshots":[],"snapshot-log":[],"metadata-log":[],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":0} +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata","table-uuid":"95afe7ed-d95e-4b35-b24c-042dc999dce7","last-updated-ms":1775656363845,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match_id","type":"string","required":true},{"id":2,"name":"competition_id","type":"string","required":false},{"id":3,"name":"season_id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"snapshots":[],"snapshot-log":[],"metadata-log":[],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":0} diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-39d25845-5de4-4f14-8523-98060401e712.metadata.json b/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-39d25845-5de4-4f14-8523-98060401e712.metadata.json deleted file mode 100644 index 1826cec..0000000 --- a/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-39d25845-5de4-4f14-8523-98060401e712.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata","table-uuid":"34db1268-9881-4ad7-b8b2-e44d1fdb5614","last-updated-ms":1775656038756,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match_id","type":"string","required":true},{"id":2,"name":"competition_id","type":"string","required":false},{"id":3,"name":"season_id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"current-snapshot-id":8301423118246886670,"snapshots":[{"snapshot-id":8301423118246886670,"sequence-number":1,"timestamp-ms":1775656038756,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-8301423118246886670-0-58e4f0c2-e0d0-47d7-91b8-84d9f37863fb.avro","summary":{"operation":"append","added-files-size":"13474","added-data-files":"1","added-records":"1","total-data-files":"1","total-delete-files":"0","total-records":"1","total-files-size":"13474","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0}],"snapshot-log":[{"snapshot-id":8301423118246886670,"timestamp-ms":1775656038756}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-8ece4efe-b333-4c57-a371-74456e7c8d29.metadata.json","timestamp-ms":1775656012055}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":8301423118246886670,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":1} diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-cb7462b7-5f2a-4530-bd47-a196cd47b46e.metadata.json b/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-cb7462b7-5f2a-4530-bd47-a196cd47b46e.metadata.json new file mode 100644 index 0000000..643b71b --- /dev/null +++ b/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-cb7462b7-5f2a-4530-bd47-a196cd47b46e.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata","table-uuid":"95afe7ed-d95e-4b35-b24c-042dc999dce7","last-updated-ms":1775656370209,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match_id","type":"string","required":true},{"id":2,"name":"competition_id","type":"string","required":false},{"id":3,"name":"season_id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"current-snapshot-id":8842646200136284049,"snapshots":[{"snapshot-id":8842646200136284049,"sequence-number":1,"timestamp-ms":1775656370209,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-8842646200136284049-0-7e31226c-e1c9-4c2b-8a42-09812bdcba85.avro","summary":{"operation":"append","added-files-size":"6693","added-data-files":"1","added-records":"1","total-data-files":"1","total-delete-files":"0","total-records":"1","total-files-size":"6693","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0}],"snapshot-log":[{"snapshot-id":8842646200136284049,"timestamp-ms":1775656370209}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-b6d20000-501c-45f2-9996-77ebcb5463a5.metadata.json","timestamp-ms":1775656363845}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":8842646200136284049,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":1} diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/00002-570bd3e4-c402-4825-a184-fa65adc17cda.metadata.json b/examples/idsse_warehouse/tracking/match_metadata/metadata/00002-570bd3e4-c402-4825-a184-fa65adc17cda.metadata.json new file mode 100644 index 0000000..02e6fdc --- /dev/null +++ b/examples/idsse_warehouse/tracking/match_metadata/metadata/00002-570bd3e4-c402-4825-a184-fa65adc17cda.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata","table-uuid":"95afe7ed-d95e-4b35-b24c-042dc999dce7","last-updated-ms":1775656376239,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match_id","type":"string","required":true},{"id":2,"name":"competition_id","type":"string","required":false},{"id":3,"name":"season_id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"current-snapshot-id":4686198960413679258,"snapshots":[{"snapshot-id":8842646200136284049,"sequence-number":1,"timestamp-ms":1775656370209,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-8842646200136284049-0-7e31226c-e1c9-4c2b-8a42-09812bdcba85.avro","summary":{"operation":"append","added-files-size":"6693","added-data-files":"1","added-records":"1","total-data-files":"1","total-delete-files":"0","total-records":"1","total-files-size":"6693","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0},{"snapshot-id":4686198960413679258,"parent-snapshot-id":8842646200136284049,"sequence-number":2,"timestamp-ms":1775656376239,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-4686198960413679258-0-fb416f68-89e7-49e5-8303-1c604053a6e7.avro","summary":{"operation":"append","added-files-size":"6399","added-data-files":"1","added-records":"1","total-data-files":"2","total-delete-files":"0","total-records":"2","total-files-size":"13092","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0}],"snapshot-log":[{"snapshot-id":8842646200136284049,"timestamp-ms":1775656370209},{"snapshot-id":4686198960413679258,"timestamp-ms":1775656376239}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-b6d20000-501c-45f2-9996-77ebcb5463a5.metadata.json","timestamp-ms":1775656363845},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-cb7462b7-5f2a-4530-bd47-a196cd47b46e.metadata.json","timestamp-ms":1775656370209}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":4686198960413679258,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":2} diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/00002-5ae3d21a-7558-4e44-8a81-70d8f3c5ada2.metadata.json b/examples/idsse_warehouse/tracking/match_metadata/metadata/00002-5ae3d21a-7558-4e44-8a81-70d8f3c5ada2.metadata.json deleted file mode 100644 index ddd8b14..0000000 --- a/examples/idsse_warehouse/tracking/match_metadata/metadata/00002-5ae3d21a-7558-4e44-8a81-70d8f3c5ada2.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata","table-uuid":"34db1268-9881-4ad7-b8b2-e44d1fdb5614","last-updated-ms":1775656064770,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match_id","type":"string","required":true},{"id":2,"name":"competition_id","type":"string","required":false},{"id":3,"name":"season_id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"current-snapshot-id":7688096930835682381,"snapshots":[{"snapshot-id":8301423118246886670,"sequence-number":1,"timestamp-ms":1775656038756,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-8301423118246886670-0-58e4f0c2-e0d0-47d7-91b8-84d9f37863fb.avro","summary":{"operation":"append","added-files-size":"13474","added-data-files":"1","added-records":"1","total-data-files":"1","total-delete-files":"0","total-records":"1","total-files-size":"13474","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0},{"snapshot-id":7688096930835682381,"parent-snapshot-id":8301423118246886670,"sequence-number":2,"timestamp-ms":1775656064770,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-7688096930835682381-0-120f7d00-f8c4-41cb-b0b8-3148f44c2ab1.avro","summary":{"operation":"append","added-files-size":"12632","added-data-files":"1","added-records":"1","total-data-files":"2","total-delete-files":"0","total-records":"2","total-files-size":"26106","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0}],"snapshot-log":[{"snapshot-id":8301423118246886670,"timestamp-ms":1775656038756},{"snapshot-id":7688096930835682381,"timestamp-ms":1775656064770}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-8ece4efe-b333-4c57-a371-74456e7c8d29.metadata.json","timestamp-ms":1775656012055},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-39d25845-5de4-4f14-8523-98060401e712.metadata.json","timestamp-ms":1775656038756}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":7688096930835682381,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":2} diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/00003-50271329-90a1-4f37-8af3-8be2f399c28b.metadata.json b/examples/idsse_warehouse/tracking/match_metadata/metadata/00003-50271329-90a1-4f37-8af3-8be2f399c28b.metadata.json new file mode 100644 index 0000000..f3deda0 --- /dev/null +++ b/examples/idsse_warehouse/tracking/match_metadata/metadata/00003-50271329-90a1-4f37-8af3-8be2f399c28b.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata","table-uuid":"95afe7ed-d95e-4b35-b24c-042dc999dce7","last-updated-ms":1775656381701,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match_id","type":"string","required":true},{"id":2,"name":"competition_id","type":"string","required":false},{"id":3,"name":"season_id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"current-snapshot-id":5701652792860281111,"snapshots":[{"snapshot-id":8842646200136284049,"sequence-number":1,"timestamp-ms":1775656370209,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-8842646200136284049-0-7e31226c-e1c9-4c2b-8a42-09812bdcba85.avro","summary":{"operation":"append","added-files-size":"6693","added-data-files":"1","added-records":"1","total-data-files":"1","total-delete-files":"0","total-records":"1","total-files-size":"6693","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0},{"snapshot-id":4686198960413679258,"parent-snapshot-id":8842646200136284049,"sequence-number":2,"timestamp-ms":1775656376239,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-4686198960413679258-0-fb416f68-89e7-49e5-8303-1c604053a6e7.avro","summary":{"operation":"append","added-files-size":"6399","added-data-files":"1","added-records":"1","total-data-files":"2","total-delete-files":"0","total-records":"2","total-files-size":"13092","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0},{"snapshot-id":5701652792860281111,"parent-snapshot-id":4686198960413679258,"sequence-number":3,"timestamp-ms":1775656381701,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-5701652792860281111-0-d85c7ea8-2fa8-4a02-9d11-6aca1be81ea1.avro","summary":{"operation":"append","added-files-size":"6343","added-data-files":"1","added-records":"1","total-data-files":"3","total-delete-files":"0","total-records":"3","total-files-size":"19435","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0}],"snapshot-log":[{"snapshot-id":8842646200136284049,"timestamp-ms":1775656370209},{"snapshot-id":4686198960413679258,"timestamp-ms":1775656376239},{"snapshot-id":5701652792860281111,"timestamp-ms":1775656381701}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-b6d20000-501c-45f2-9996-77ebcb5463a5.metadata.json","timestamp-ms":1775656363845},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-cb7462b7-5f2a-4530-bd47-a196cd47b46e.metadata.json","timestamp-ms":1775656370209},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00002-570bd3e4-c402-4825-a184-fa65adc17cda.metadata.json","timestamp-ms":1775656376239}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":5701652792860281111,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":3} diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/00003-d74574f9-df9e-4b34-9eab-b38255059e3d.metadata.json b/examples/idsse_warehouse/tracking/match_metadata/metadata/00003-d74574f9-df9e-4b34-9eab-b38255059e3d.metadata.json deleted file mode 100644 index 24929e8..0000000 --- a/examples/idsse_warehouse/tracking/match_metadata/metadata/00003-d74574f9-df9e-4b34-9eab-b38255059e3d.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata","table-uuid":"34db1268-9881-4ad7-b8b2-e44d1fdb5614","last-updated-ms":1775656087730,"last-column-id":4,"schemas":[{"type":"struct","fields":[{"id":1,"name":"match_id","type":"string","required":true},{"id":2,"name":"competition_id","type":"string","required":false},{"id":3,"name":"season_id","type":"string","required":false},{"id":4,"name":"metadata_json","type":"string","required":true}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{},"current-snapshot-id":7183120188202263762,"snapshots":[{"snapshot-id":8301423118246886670,"sequence-number":1,"timestamp-ms":1775656038756,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-8301423118246886670-0-58e4f0c2-e0d0-47d7-91b8-84d9f37863fb.avro","summary":{"operation":"append","added-files-size":"13474","added-data-files":"1","added-records":"1","total-data-files":"1","total-delete-files":"0","total-records":"1","total-files-size":"13474","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0},{"snapshot-id":7688096930835682381,"parent-snapshot-id":8301423118246886670,"sequence-number":2,"timestamp-ms":1775656064770,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-7688096930835682381-0-120f7d00-f8c4-41cb-b0b8-3148f44c2ab1.avro","summary":{"operation":"append","added-files-size":"12632","added-data-files":"1","added-records":"1","total-data-files":"2","total-delete-files":"0","total-records":"2","total-files-size":"26106","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0},{"snapshot-id":7183120188202263762,"parent-snapshot-id":7688096930835682381,"sequence-number":3,"timestamp-ms":1775656087730,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-7183120188202263762-0-697521bf-42dc-4f0b-9994-9661a5bb2874.avro","summary":{"operation":"append","added-files-size":"12579","added-data-files":"1","added-records":"1","total-data-files":"3","total-delete-files":"0","total-records":"3","total-files-size":"38685","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":0}],"snapshot-log":[{"snapshot-id":8301423118246886670,"timestamp-ms":1775656038756},{"snapshot-id":7688096930835682381,"timestamp-ms":1775656064770},{"snapshot-id":7183120188202263762,"timestamp-ms":1775656087730}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00000-8ece4efe-b333-4c57-a371-74456e7c8d29.metadata.json","timestamp-ms":1775656012055},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00001-39d25845-5de4-4f14-8523-98060401e712.metadata.json","timestamp-ms":1775656038756},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/match_metadata/metadata/00002-5ae3d21a-7558-4e44-8a81-70d8f3c5ada2.metadata.json","timestamp-ms":1775656064770}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":7183120188202263762,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":3} diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/58e4f0c2-e0d0-47d7-91b8-84d9f37863fb-m0.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/7e31226c-e1c9-4c2b-8a42-09812bdcba85-m0.avro similarity index 92% rename from examples/idsse_warehouse/tracking/match_metadata/metadata/58e4f0c2-e0d0-47d7-91b8-84d9f37863fb-m0.avro rename to examples/idsse_warehouse/tracking/match_metadata/metadata/7e31226c-e1c9-4c2b-8a42-09812bdcba85-m0.avro index 286081254041072397fb4fb884d82463daa12219..e672ad72c39791bba86916c59e9d048f2800b83c 100644 GIT binary patch delta 300 zcmbQMJX?7~r9gdgjhJdT*V(sUUDh`>cE5FC`p4Ycv)AvifkqGmL&;2~Kz--U8&`As2W=0-XujsD~UHmMpxqeURQ{OpFtr1~M zSwnYOn+064lv)>*Y*5G08@biwhxh~5h?z%OqcRrPC_7r~B)H$;Z?%}h<7}D2tSKaR z&|yP@M5~f#D$~V-b-cgwI?J-n_F2TsoPQbZR{QJb$60xg*MIL|esytO^jcME?+)hv uJb%A)H%e^MmplG1s(Sit+e=yE{uBvJ~%SYGO%Fn)NyL(bfCG{<)RMP8>K;(5I_Ak*ew6r$Xx3yE(?U4z$ShKiKL%cx}%Qm4K@5$wm)jLr*g=AEPVU9y(e&zPb;&3wXGAN_i7+ z6-=4f+5D1C*_$<*_4sNHHih=i)=plJ#u+>bYRt?N4j)o^{OH-@*RLfOy-rP#d6#}- mVZd6E`Ln_&MNXO-wI+r2iQr-n$qb1TE^HzUp?9Lu9SQ(dg->(< diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/697521bf-42dc-4f0b-9994-9661a5bb2874-m0.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/fb416f68-89e7-49e5-8303-1c604053a6e7-m0.avro similarity index 92% rename from examples/idsse_warehouse/tracking/match_metadata/metadata/697521bf-42dc-4f0b-9994-9661a5bb2874-m0.avro rename to examples/idsse_warehouse/tracking/match_metadata/metadata/fb416f68-89e7-49e5-8303-1c604053a6e7-m0.avro index 755b059e089acb8f51c3d539ed0ea826392c8d1b..5dd5b94a8dff1c9aa7cae871c2941d6d30c68a51 100644 GIT binary patch delta 301 zcmbQMJV$v$r9ge<1G}adA00QiZC|#{>i&*!rUsVeq+6e|k4^q2Ty8Z>LQ+EFoZ-^c zXG@}9zJA>i~wYbPrb=9R}OV!e+h`iptWZKeaPsEpqy__zhs+OMqZ0YgkD$`Vs zsXu={RVD2inf z+6Pt(HrBRXZ3ch6(;g-4I(Q=JVA#QrzLObGBqk*$F$XEAG0!-8fZ@pogOfXU{Cx4t ws_jLUq(u8~$s&yq?W4<|O-)gqmaTh(-{b6r0gRMuxBnQRsmJ0JZ;&7ytkO delta 302 zcmbQEJXd)`r9i!vrMKEOgT#o`64)WnjU6lL`D-OHQuerxQ{oj7oypifiR zSpBM3e7yY@i>)gn_Ak(|*VNpqVQRT}NyL5CtvaUa`xUl&>|df`XlZS%Z)>NzQN_^C z(tf>)p)tS4yr_vYq9;s_S~GuUP|O^u_<0diW=)Kbn!|N{!Ti|~^^@iZ&YvAMiR02L zel;EkhwBd4&s@55<%EHz@AV@Zs*%@K&MJ6bzjW!6%k@iV&KO)Y(L8zL&>ffc3L95! zFxlFof51?~;EHL>%Nv4jYJAJ2dvru|L}hj_l4y`o6H_x&U`;Y=n90NAz{fqgr+aq) xy7kE&>!vpr?%2=X?zA7f#d)H diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-7688096930835682381-0-120f7d00-f8c4-41cb-b0b8-3148f44c2ab1.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-4686198960413679258-0-fb416f68-89e7-49e5-8303-1c604053a6e7.avro similarity index 77% rename from examples/idsse_warehouse/tracking/match_metadata/metadata/snap-7688096930835682381-0-120f7d00-f8c4-41cb-b0b8-3148f44c2ab1.avro rename to examples/idsse_warehouse/tracking/match_metadata/metadata/snap-4686198960413679258-0-fb416f68-89e7-49e5-8303-1c604053a6e7.avro index 364a0d6d7f8e2d8231d73744aa5233afefe240e1..e5af0a1edb44c33d409ed613c654af8011e9efdf 100644 GIT binary patch delta 350 zcmaFI_m0oUKPiimN31w6v7k63zeG1PMa{&_!pzXp!qUvZ#L(Ex+|tO@LZu+FC^fG{ z7g?Ewg^7`wiJ6gs0Z^5Zg^7WQ3ycPYb$nXFDM$C!`y`Fb5O zkZ8-E`TEEole0_KrRF`@RpYt;mQil-q%#(;Z@v&L(XL={ydun_XVbLf6PMDCO-g<4 z0{4!dX6Coqy(Q1uX3>;+EMFS??nySwD_M~XQ|lV{u};R##paq6U$EBjxC1-c!LDOGW2{> kd0+h5{;zJ8|K-=$XRm(Vf4lwr{$KhXyIWx1=79Md01%#+fB*mh delta 350 zcmaFI_m0oUKPiimN31w6v7k63zeG1PMa|sI!otAP%+lDv!r0Wz!pPXdP^BQTC^fG{ z7g?Ewv4Nq9k+Gqng^>wRhnbnV!A2h$w))biEJsVyEqIDl^J2Gj>-)1DV?KJ$(C@H; z1nUEVuU5~t3p?LA*CB1-zW?ogl`J9mZJII6iegv16$};lE--dBF64eQr=ho1!Nt;P zskx2qOV9gi<+<+M3>MuYN<9a&eH(oyE6uskWn~}KAaX(H_QO5fW^6F4^SHrx*)RM? zzihp0Ms80?eA0qr@h-Py7aUKSwQG5cefSO0z=H6|b2Ee8j5K@FW@R7sQk**ZlE@0y z*FP24HPzdk-E{NKyRUE4{{Ac4Z+l|%8}r$F7HSehA_7#f=aRhwFD^pRn!chQ)wxuW?-!n_mPc5IV8x|(ef%h?Niy^c5t zuz!%~tDaqIzv*Vqnxp0$_%_e(6h9-r`{R~v7u_$oxoi<)c^Kfm`tTul0c~z?X~8u; z8q4z+81NjH|MwyJSz=(=4kyjn?=NiL#$@wYZJB)Ua_F3-t7R`T-XF-DcGhj0Gylxo z*>b1q(@qq!zgp+yfBH`KnQaFjF1h<@hsFh|D(jP3Zys*;5_eyhJX;|{sVmVlp=pWb zbC*WeyntdBzpQOM!Pn!~vL39cDg5=vXt%AsT@+XIgY0MZOZ9u6tO}U*LPS_8xTmu( z!evLO&n62d(2 dR{#F~Hn6F*vEEa^dr#fpi}eiqCmlx*B>|+{0n6D7d%ai; zMOYscMtsXnnl$r_%0{vK2dYk-@3pq5S9$y@`$W>>=v&}E#b*@;fm93yjPTUs9sZe*5MZ9 z)s`|ZcxvWcj&lLN*REdK9v&TNA~*lsy!U=_`RCSMf28qi*U|WwnNBj>TNKoJoO`5t zO)fGmox*v1hUb)J_kW#sw3m{#w>kIB|NHaP^$X6k@HEdhWntXNVwh(+spJB8+EVYG fCxVyP=gS}ey#4Ug-xuE=J>CD0nd#ec^gsdtm|wIx diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-8301423118246886670-0-58e4f0c2-e0d0-47d7-91b8-84d9f37863fb.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-8842646200136284049-0-7e31226c-e1c9-4c2b-8a42-09812bdcba85.avro similarity index 83% rename from examples/idsse_warehouse/tracking/match_metadata/metadata/snap-8301423118246886670-0-58e4f0c2-e0d0-47d7-91b8-84d9f37863fb.avro rename to examples/idsse_warehouse/tracking/match_metadata/metadata/snap-8842646200136284049-0-7e31226c-e1c9-4c2b-8a42-09812bdcba85.avro index 98c58a19be3abe896ef3cb377e88d565739a89b8..c7db66df011c6df6e33e89af87500a60f52433ca 100644 GIT binary patch delta 249 zcmZ3=w}?;GKPiimN31w6v7k63zeG1PMa{y(#K_FV%*epN(Adn#!ob$9^U|myBFKGf?n%dj73HCur?Rq5 zxn;(-~DmpFgjv?~hntzc1~$6S~U*;R$U+ delta 252 zcmZ3)x0FxSKPiimN31w6v7k63zeG1PMa{z4z|h3V*wE0z$i&RT!pzLvV56!DTYZ~u zO!3BOlq*U+wWRGW7R4$s?cq{!OWh#+pV@jLz2mF$!b(8IU_#0YKBPiWIn90%}cnzhf+)wd8r zDItm%y~K2Xy19X{mQ7*!pA|uda>Qr_pJ&6cYskp3iw~xlL@RL|oN(v3}DkUeD#Rk8`?H^s!Y8?#UfoDA~kQt-R-v+$?s9 z>6AUv33MmbHU;eKX3ol=nmBz{IhUSRrjmnK&iAWHx|F0sbR%!*Cy6ph^prAWXrqcY ztPdzDEv1c&4CjAGhcxX(3A?w-#E)G}U%r%`oMxo%Vh>)t3O%fC9A#u?nPr#R@KywF zRT~km(i{Cz{|#fc2&(q-A0xuM&F4L0ZF8HA*Pk3reEIqzB&|Pw!^-y$fgnulu#_+b zQaDPK5~CmrPZ200B~D3DB#KPwt_C3BKorCP0{r{=N#TFzN}m|)_`;v3p+sY551ME!-dJll z;S*dyJavD}bP9xr)b{%s5gJBcCJsM;T1a=^AD7VDnV>HbkIPagHb)8O0O9NHCbz!x zOnCIY6$}Ra1@l4TDTp9$vhh3p)|Kp(4$#B60`v!MkI!opK@`VlCp8Sq5~j-%LRhxm z>cMr>v`K$JAPvz&@mHnMi@3~gX5F<(cHP}r3q60767Z&ffJF*HPerdDDwN#4h(9iR z6@(s&P!A;sUs4E-_~7#%41C`E4#xYhyC=H`J0IQ%2%$c9yarz~45JqDFfxMBw=5oV z+l?cm%AfFtAMntKLuRjgt%qiv#muz*R?NH>yz{5588mogc=aga<^~J7>$f9562?tq z*qVPVwq4V!m#WvU>o=Cy^p)J~y_+*eZczG+&TXge*b7{r<#s`zbF4+(D%yFyU=^(S z`C@%>&a$oqEPUMNaq9N+oz>daHLQ*=JNkI})w6dAtl^F@(Gilz0y+J(E_M>qLHDJQ z`Yh0|SpF_3KX>x!)4rbR2-){vrqu0U)7pRY1Og2fOaMuM03sj(DS!eppa5wg17rae z(17zgB1l9MN+F75r2Otqrh%aVQw21HFv6MSKlurz(`Zcn=WfO5=FJWAN&0a zC?y3Aw#h>}^lnvF^jt2#P%X-YZLm#+9If8%rHSKYdRhAC()|~`lc__Mo^3?i+QnLN zYs>v_gyTJY7J){t@5g>Sh5(1VkKPUfkeIV3l?Q!ka8o9(-{cBG=s_h-O5$W?bh)h3 olK8C~l*(mI9SK{52EWIkpue(%)hUP|ZnE(^{nnN2ln&6txLHLn;s5{u delta 1122 zcmV-o1fBcgFX1n+j}w3H!aI$P6kM6=5)rLVsj;mBzzAbv+OX%#neB5=P5Z#ez`*c_ zDJ?T6)k-KckzZPz3fF+DQy+g!iJ`7}lDV<2iD62Lu7w$pWtnCP1O_Pvi76?1 z1&Kw4rKu%+0ggd|p{^lJGN^`iEnLpLVcBdYCOM`>EG&yy*eV4~XCc5dt7W2tL0V1S8gbav~0}-gEF>o+&G4L?(F$gdSU3+aTa_69g z7!XU$_&b$Z3doQFVmSr|CI%)cHZ-qs2neV%fB~0CT4sMyaY=keVon;507K}4yKDZi zGVpOECFbM^fOYvhdFg^e)hJ4cBe6WOQiOwnA?}bE$HOX-LlQtXLxYsSH;tJ7Lto_& z$Ou48WoVF7K-F85fz=rw7BE4aAyAx=pI?$+S^{*$fjL?0z>X+MP0SS#0+Ci3`MIe= z93T>GzmtC$M_QVolLW`vGcz3F_B+T3taAstODxerP6n>621EeEfrDet|HE7>K28!{ z^79EWU>E>+BIEADJB^JLT$$<;5v@+Cv8@952W^i*Xwy&_$6ubaJVGdMA%s#w^yUO( zQa8G8Q0U6^FmyUu^&*DX=EX$YCMKzKu!Beu4~l=com7VN;6cQjC>}-;4>Iwnmw|ZR zp$z9idhlxp#fl&NzV~?W`@a9f`1Sjr+i#wHTJH)7p&oYqI-fO7vl8z9j!l-Eu zT7!QjHP7~);{2Jj#<{s=W5JrdyqGhsei=C<=T`Ii!c@+fu1-%HQ#r>m3VFEAxHC|e zV=>1$8M5eRizn#|a~GE?r~9Kt$o3uA2|)fLFIhoRWR(vO|zLV8c2 zAF=#hP(HjZsOxtpGHoHd{qDH-;+v`e#t45j7%&E;00M}B1f&59$bbTxdu`NhpmdlJ_>pmHYQZ6&NURYJiRqY|Nzo`2nHy7#ftQxPBBTw#VwOL{Vj9=Vk9O zN=sp#t@4Ntyz}J+!?I2l7RxeWD{NIEyGxfkDskORCrf`#TzS;lKeDCKgNf*leso&Y zTJ!!Jp}T<(BGA|if+T1q5Mb-^^;i7>B;l+<<$j;-@05ubG`K<#x>{CAQ5-K1H-YU;g_7#t36#dNO@+*T#KsZU`_iF#KUk z%gjl&(%07yElw>e)-Nc^PfAKHN=?ovDb{yMElbVGFGwxYFDXh)&d$tBk55S~NsLd< z&nrpH%ma$2RwU*Y5 zD~bWJ#Df15nWcaX86cKpU|?ckl43*i8i#;@Is+JRiKKsJ78RGoXC&sN@dzB)FsDTQ zIV%GnM^a)=jsRGfzmu0PC{&Fjgg6q*6DvhH7#QLXiE;EF5<4UTWHU5K34GIt=|A*U z{(y`C#8id`IR#X`H5pi)k=xG%afU!~Mt*)terXAh0K=v`-!#CEC`nDs6%YcERvG!Z zsX`ne5^PGplNiUtDlsPsj+p)_j&S=OWCU7Y2kwx&o9rMb1J_mqB7ot*!Ex?S7gyiP gZ^AFy_W}ck0RZNc==$GgxmuL0B_@XcgTr6``-g%700000 delta 593 zcmV-X0oT5>zI5 z5K)BvVR+GjvfDNf1I_E8Q1QY0@Obe3z2EyW%<9UcH?NlW?r(n|0s!nYCCerws;W+V z#P!sw>*zXhiDCMlI!3M&+o=*)^<8XSwJKK%MeO5(;Z%HVRl<961DC5d@l>nmd89Cl zU1B;lk8}dvNwrNG`-WMtiYLcTpH9?;PJGk3=eCA zO16YEN>E%nBKExw0|)hK$}%fE{WD_`&Sh^@D4Y;xfvm--%jiAW#TmRY%R7=&S3howX* zkit>olmrD)c#1#~DM?C-B2i>YcQpV32jUy6Zvt4PIc%K5AiI%DL?=& z2O_KoVvr2aU_OO*2WINQ&XJac4yuBA`Dk}`&irp17tIr&oFFh*sm6Pu%ibAWJbBjE15 fXbR8v7Q?}SzjvGJ2&lJpdHVEDt7 zmYI`krLV6aTAW%`tY1)+pOlnZl$xATQmpTiT9%rVUyxd)Us9BqoSm7M9-op}k{F+y zpI4HYnFkb4tw_u*$Vn~M&rB&UPK_^5EK1GDFD*`mYe3bh4<>(d6HAga;xkihUEF+h zeH}w|y$p=QeIxV@KtR_(H!ayR(ZV3nOgGISHA&aR$kIU9!ob`>*U%&>)iTAxAk940 zT(2OpsIWA(gfGA`C@|DDgh>X~u)gKXnO|J}qQoS}w1|ae5ewTTc8*mSkFs+u;N_XO zzlD?U2)n==UZH>}HqEn*e>aPlgPM9*JtsRbu)Gs`^teu72r5kCV^Gsqkk5Wxl_ zI6wp!h~NPcd>}#qL2B9B|+(l;0 zl@J4Bi4FUvGD`s&GC(ZHz`(@7B*lj2H4Xs*bp|lt5=no{EGjOE&q&Nk;}JNJk$r2; zA65oFj-YqG5v?W z${&ysfSAhAAg6$;wY8ZBT7;ea|MJzq*X?K zZmJLmh!lU}5Lp-NB*r1%D()n~@v!QeBiw!m8G+WMuG9Nchb0OqG zXx0M1SmXh>-6%At`CVT2YdkQcfY~cvb-7St5i8h!HDX>B?)e(4)XF?Gy;2zR!YT{6 z>o1rrO-x_CZp=>28#C6W+p~8}D=y=r;n+Erwb+>9SiESAj^r#O zYh~bNw8(R%tmR~Q=0c4H^#+fmn^U*u7Ou==t-smc!L^q^zYk#@H-*8bkoYB#)}w79 z`EWpd{N9SZaZmcI$g)|c-kf&t^OF8#0D%V`27v@XfFvLS5&snOO>T$d+xttV{60l&utM*Ias(br}dmIv;RgFjQ<8>Ca|6VOCe3Y!1` delta 1127 zcmaE*|4e^Fzi_=(GE(Wmloet1^2tV3<8>y+Mg|6d61`5J z@!Wdp`gML?KV7YB`WmPFy}We2e9viUUMu(1^*(!2-$VD3zNVMYxpQZ{`~5w2JbV1l zo!9m}bB4KVd6>#Gu_sH!UZ*b;5nq06y1LlXv?tTWmLD=*@ahVCz3|RX)svDRXKHTK z%d^|HuTODT9NV#n`viZnDJ;ldke!m0keD$sXJz(;S#wroCuPi8Gdm-5-m=*#ITLdd za%N7-N}RPKd_qJ>#H^@8;twQ5Bwk4!QfbbLxc}+tnbrT!gr`hOjyiH^Q%CDtA#3;G z|M_O?D)-o&=6m<_!R=#u9~#2zGuU^Zn0>A;Y-8KW;}I&q{uO!W{wiyVOuIJmu1?YQ zACe8T9G`7y;4l+7$f&f#frY>00ka^Jf)j^JM+1kUfFh&P5{DL^jtR_yE0!4+7!(+O zF!*4|VZiY>;)&j-=ZiK;2zck*FrOO9bz%Zzk|i6P8k?G?naTP>4vrPZ4UE%0>Z8&E z3q5PxlKiCl4>b0?iO&AF!RSMwit4i`91W#Ett($PIbFRR*rB}9W@Avx;;*G|6b<`c zceC<)7BKW?Oc469Oz?93i`9P^B^f(b^6(aMt*FWt+Gv%0O-il9n5E13(`WBb-rf}~ zQgWK%YnZJUshApuJ8-x}r9OGSjH|H0qh642V(CV|BpGk6BXd+NcX zs-Qh4tE+hqPfRp#2SXb(ySwvs#&gSF$|?z`$xdU9;$rv|{1hYgd|@Xo_0&_AeDJ28 z6-1<-daTLk#=6b-Zkw%7Po9*Vm6(#5nDYC;iH{i{OWfA6p0W9V%irjeY=TI|QI(SR zz?A#f#oRXP^RHHkl2|JDr(?S1M-~4(F=OTcN|IEV#$2D2@|H)2Cx*v{r-p}#$B9RYCy6KFD31|O5f6_KkB?AJhTd}l zfq--dX1$49&MbN~=||6zIVN(A%#t^^U;C4o`rxtV3C}e?xH#M(A%;?70^ z_oT|Cr5x=w*=K;srS#pR_9ZO)GLL+b|D{#$;Kq=oA3&^ diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4743469712033335616-0-240818c9-0eca-413c-a97f-b38c039de85a.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-1282166195388124131-0-fc9a80a6-f0eb-4290-8070-14be9d80f7e7.avro similarity index 86% rename from examples/idsse_warehouse/tracking/tracking_data/metadata/snap-4743469712033335616-0-240818c9-0eca-413c-a97f-b38c039de85a.avro rename to examples/idsse_warehouse/tracking/tracking_data/metadata/snap-1282166195388124131-0-fc9a80a6-f0eb-4290-8070-14be9d80f7e7.avro index 873d06c80fa69b803c3851b750c6d99f0d0bf706..ed4932e29ccdc8826ade93ae8f33903dba312403 100644 GIT binary patch delta 181 zcmdnXw}(&FKPiimN31w6v7k63zeG1PMa|I2!pP9f%+S)**uuil$i&dtaHFaTn~=uE z?MryMkL)phBk!>D%6{vURYXUg2l(D!p0swj2x*kcWO#% YWt~g*^7I_8Rle|(hlk-kzYn^t04B6c0ssI2 delta 182 zcmdnPx0g@VKPiimN31w6v7k63zeG1PMa{(A#Ms2l(%jIy+n+y!*{P@%@pgc{JO?>U8Er!)=KtBf@5Tc^NHf>rJhd3XHp&5|P|;>*T4oDx$p} zA)#jVO4_&QeRB$& zl`bCMH~?K`uMYqK delta 384 zcmbQtKY`!JKPiimN31w6v7k63zeG1PMa{y@)Y!<(%+k!<#Mr{z$i%|JLZu+FC^fG{ z7g?DJP};=I(%jIqAqf&77RI zbp3kys5wy~YvKc@%%3tPYRarRfkA8DPl}p0YjXUAs3q}1Q)bMWJ9E0+{7Dg$B<9bV zA3kZuOm-3fwH{}5Pk8EHJMW{Z=hv(6uj6(0gubp{YqrDHmGfsuP2xzCSf)C2X5Milk<&7#IV4O-n!q7)()5D2j`k^gk@E)*9NLt4e!B7P=ksQL zYn<5H`0)1b(`R^ie0+EuG#)>Cw)pjHiAAqd6J*|{cbrR4urP9{GchvUV6eqw+Z9s} zUq6;#7cC;qFJ7|oH9vVn>E?kOMygj2nFu~RZv0W%c>CP{|Ra;&rY_*A&e S{+HW=&yo@h`TJ&|hYSF`nxt6( diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-601469116504892914-0-f58e70eb-6eb2-4a19-9b08-5b01b851d469.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-601469116504892914-0-f58e70eb-6eb2-4a19-9b08-5b01b851d469.avro new file mode 100644 index 0000000000000000000000000000000000000000..63e44a5d71cd903af670ed5dc8febfef598d13b4 GIT binary patch literal 2001 zcmeZI%3|aZE6z(SD9*?)(alUzF*7hUF|#x@G&40Yv9L6Mt1YHny~Vq$IpgJW^CZ!fJ8Ox;Q7v(0F=$54x6=&w>F&T*@ zmKEjeCFiH4CiA7FrsX7-q>6!sijy-^a}(EzS1XlN7Nja!DJT`CCg&HWDCsCDrDdk( zq!a_iqN|ni5_7?_xruq1X{p5}@db$`8E{2F=L40Q8W;d|!OSTxDay=C2PsL(PX-z6 zlb@Ve0(6Z+XppBud1gt5f?Kcx*#1;$g`Io1cw|(4Ur>lUf{~oL`FReFGCD{TN}ET9H{?0*vf@ zo_q|D(q6PuUTIDaPz55xCKeSXR)X{Z!!Q?ATtG9U1yCrnBsCY5gAuCnXKzsIPt43K z1|~zWEs!z*W}+ohB1p>5&q+ zv-Pnay=d#r>?pwY;3Ln$qUcXcQzO~FrK+s-Fwe0x2duuFxiXOh}xi+m#ENSwo z1+5&D;&fFy5>_sl+q*QPBPy~&P)Xuad*(CQithH8QdVmo@hmOTJ!rA8*J6@Ta>!JZ zg{nD8rdbI-mm^l3-LUT=yOQX>u2(BLUf+~0cUc>Fw582)(t`cENo!o9Isb|NGtcA= z{_a<7Sqn<;;W;jW-y;7bQ>eow)vzB;;=6Z^DMT%vca6E5&R zW%uzG`-!LL`fkr(+kN-+uVqR1OER&XBl$%(hTb5c>oSC1;WF(SU zR+O)ooS%}K%$JgymXlbLDh3uRPR>ZpOQ&3BwKRwfp(zi28LNlCdi)n z;)2xVc%aK*UV*vN2uXitUJ0u9K2A)5W=&K(|7p z5SIyNcuh!5Nl8tKhb0qiel{jTe_CcvYH@sWekrE+4NQ>qV}x01MP_jcFtX$EnSdMy z7$&5o=A@PYy@1aE01^U4`QY(`_+& z@-aY4d(ldHr8zl36^IO*SX7i)3DN@$!(32t0nLmSK%vZ%)Lc*wMyST0y+NrzF*C0i zm<++TK*|7^iIzx-ASpjTCp9q-GxI>S17(1U!6gV(w;{+q(82-k3niE_u|P>+!IW5< zQv!4@$aa)Wos(alS`?oIOqv+k*$}DFK{B_pB(+$HfT^Vg1vpJbDrAT-6=V^th%7QS zuu!V4C17`UYGr(GYDr>BVo4%KP$300iI&65XHZU3s%7BtIHEDhMla}^es0BjhfRfS zU93mX@AooDI2NLaP@)kk;sE(p&W4 zg_@H(XVB8wetP>H1vPig>f)OA&NkihK70C))F^>#g?)zG<|WM85GLcY!i8)vHTw7RYCwU+L`!S2peNb7kNY4JtVPQC8gX65IU7GMU&=PR*%tyVN6}fBU>4{sM<(t?hgz zm&+deE5y*+)so0xQZ69)Y#$YuvAmTJXZdw n&L64jdlNR7SAHm{FZ{E|-lUBE{o~%Ly%&8>h)=0{f*!5_ZGx+b diff --git a/python/tracking_data_container/catalog.py b/python/tracking_data_container/catalog.py index d898c0a..0427ecc 100644 --- a/python/tracking_data_container/catalog.py +++ b/python/tracking_data_container/catalog.py @@ -378,9 +378,11 @@ def _save_tracking_data( entities_hierarchical["teams"][tid] = {"players": {}, "attributes": {}} entities_hierarchical["teams"][tid]["players"][eid] = attrs elif etype == "team": - if eid not in entities_hierarchical["teams"]: - entities_hierarchical["teams"][eid] = {"players": {}, "attributes": {}} - entities_hierarchical["teams"][eid]["attributes"] = attrs + # Use tid (e.g. 'home') instead of eid (e.g. 'team:home') for the key + target_tid = tid if tid != "none" else eid.replace("team:", "") + if target_tid not in entities_hierarchical["teams"]: + entities_hierarchical["teams"][target_tid] = {"players": {}, "attributes": {}} + entities_hierarchical["teams"][target_tid]["attributes"] = attrs if columns_to_drop: logger.info(f"Normalizing constant attributes to metadata: {columns_to_drop}") From e5e3c0dee36f248db193e0f4cb29118822071d24 Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Wed, 8 Apr 2026 16:00:18 +0200 Subject: [PATCH 16/21] perf: flatten hierarchical team metadata attributes --- examples/idsse_warehouse/catalog.db | Bin 20480 -> 0 bytes ...00-501c-45f2-9996-77ebcb5463a5.metadata.json | 1 - ...b7-5f2a-4530-bd47-a196cd47b46e.metadata.json | 1 - ...e4-c402-4825-a184-fa65adc17cda.metadata.json | 1 - ...29-90a1-4f37-8af3-8be2f399c28b.metadata.json | 1 - ...7e31226c-e1c9-4c2b-8a42-09812bdcba85-m0.avro | Bin 4507 -> 0 bytes ...d85c7ea8-2fa8-4a02-9d11-6aca1be81ea1-m0.avro | Bin 4507 -> 0 bytes ...fb416f68-89e7-49e5-8303-1c604053a6e7-m0.avro | Bin 4508 -> 0 bytes ...-0-fb416f68-89e7-49e5-8303-1c604053a6e7.avro | Bin 1902 -> 0 bytes ...-0-d85c7ea8-2fa8-4a02-9d11-6aca1be81ea1.avro | Bin 1948 -> 0 bytes ...-0-7e31226c-e1c9-4c2b-8a42-09812bdcba85.avro | Bin 1826 -> 0 bytes ...0b-fb8e-4c6e-9d72-b475830bc2bb.metadata.json | 1 - ...9b-a178-4260-a16a-3dc465003124.metadata.json | 1 - ...bd-8c50-41ea-b5cc-28d517cd4616.metadata.json | 1 - ...53-47d1-4eb3-8895-324002808f47.metadata.json | 1 - ...ecfdfc8e-3ec5-4fb9-b6c2-5b5b776d94bb-m0.avro | Bin 6113 -> 0 bytes ...f58e70eb-6eb2-4a19-9b08-5b01b851d469-m0.avro | Bin 5589 -> 0 bytes ...fc9a80a6-f0eb-4290-8070-14be9d80f7e7-m0.avro | Bin 6122 -> 0 bytes ...-0-fc9a80a6-f0eb-4290-8070-14be9d80f7e7.avro | Bin 1852 -> 0 bytes ...-0-ecfdfc8e-3ec5-4fb9-b6c2-5b5b776d94bb.avro | Bin 1939 -> 0 bytes ...-0-f58e70eb-6eb2-4a19-9b08-5b01b851d469.avro | Bin 2001 -> 0 bytes python/tracking_data_container/catalog.py | 15 +++++++++------ 22 files changed, 9 insertions(+), 14 deletions(-) delete mode 100644 examples/idsse_warehouse/catalog.db delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00000-b6d20000-501c-45f2-9996-77ebcb5463a5.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00001-cb7462b7-5f2a-4530-bd47-a196cd47b46e.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00002-570bd3e4-c402-4825-a184-fa65adc17cda.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00003-50271329-90a1-4f37-8af3-8be2f399c28b.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/7e31226c-e1c9-4c2b-8a42-09812bdcba85-m0.avro delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/d85c7ea8-2fa8-4a02-9d11-6aca1be81ea1-m0.avro delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/fb416f68-89e7-49e5-8303-1c604053a6e7-m0.avro delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/snap-4686198960413679258-0-fb416f68-89e7-49e5-8303-1c604053a6e7.avro delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/snap-5701652792860281111-0-d85c7ea8-2fa8-4a02-9d11-6aca1be81ea1.avro delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/snap-8842646200136284049-0-7e31226c-e1c9-4c2b-8a42-09812bdcba85.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00000-3899460b-fb8e-4c6e-9d72-b475830bc2bb.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00001-0203e49b-a178-4260-a16a-3dc465003124.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00002-ec4a76bd-8c50-41ea-b5cc-28d517cd4616.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00003-1ebc3653-47d1-4eb3-8895-324002808f47.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/ecfdfc8e-3ec5-4fb9-b6c2-5b5b776d94bb-m0.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/f58e70eb-6eb2-4a19-9b08-5b01b851d469-m0.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/fc9a80a6-f0eb-4290-8070-14be9d80f7e7-m0.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-1282166195388124131-0-fc9a80a6-f0eb-4290-8070-14be9d80f7e7.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-5997955711447099734-0-ecfdfc8e-3ec5-4fb9-b6c2-5b5b776d94bb.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-601469116504892914-0-f58e70eb-6eb2-4a19-9b08-5b01b851d469.avro diff --git a/examples/idsse_warehouse/catalog.db b/examples/idsse_warehouse/catalog.db deleted file mode 100644 index ddd8e0c4d06a3c82012485f2d19bcd89ebf9defe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20480 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 diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/d85c7ea8-2fa8-4a02-9d11-6aca1be81ea1-m0.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/d85c7ea8-2fa8-4a02-9d11-6aca1be81ea1-m0.avro deleted file mode 100644 index 5e8f0e68cb66fcca9d3254d1a984f7688ca4e405..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4507 zcmb`KYiJx*6vvZ3v`8NcZ7HF`Wk}KlcH4O*&2IX{M`>%V(o_kXF4Ma+cbjSF)tNgf zu`c+j;)7yaThfP3(OR_@Bw!E(NvTvT_(eggpeWKW76eh$FDjmUCo?;Fclix>J@Ke6yX^9~Di&?Q2*;8QQqsE`rL1~GMC=b@|cW^hTEj-wm!iQ zOq0o)4g|XhAHZiEm{jOS?5DIOWCF1@qS}E~B3_L*%4}uJhDlU^O6`u>YV*us25Ov@ ztAf)F!FUjBgd`>58;=5G=xD%C*aCJ@m>-A*vKz73XOQD*>>q-30-!Z<<1}1M`)dt} z$#6qO^XiOk?Qk^AN)SD>Ya2Rb(0;U~2k|T;>Ac|jp;HqqBr=2bjI@PWy-i#(6KqX8 z0xT(`GVml(X0H=PXU653A~>#&#+E9Nwku=G^T3808T zJn91wLrF}ebyy@->pOWFAg7E%1s@rqF~&G;f>Za6=ZI?9YH5&?+FF*xWFGNqtQy+w zP;4TmftUs}6KbT&Q*+F~vQ_rPMD-k~jrWSU=&PBJJr57ES2rlJp#4D~i5g3_FGWE( zAW2>7fF$K&R%}P2Ssfr;D^g&s38a%$rLL6c1K13Bg+^PV_fPG~Rf;V1rnAq`u(_Kb zZ3s>-SEv+WJ>9t2LRf`QVwZ&|q=O6{Q3w7QP|gzaLpS^b>2Ps}!i1QJG7sA0j@AKX z9#G0+3~1&k^D{>&i}gmyYbOJVshNQeeS;q!2-`Z^iFc;ivSUS^3b2%DboUNemKb1p z_JCz+7J%jY0cN}q=LI>;I#kLX8{1RDRJ!V!tx67HD*4&NRB|crr&ah$zMe4IrgRKmjr0>cx7aN(Kg;WSIN#>2HU^Kc8hUBiUoqkv7G zFlXq563;Shf;{ICiaccVTl`rEn-s1IFx>g8YoI$GF!yvQ9(FTNcb|^QPDy?NnJVJj z9)nrc^@z_dy>LwXU;?fpV1iv#U{W@G?n*dPMH8O~LpP62v|wS|C=;KmAma@Rm4@?g zem3@omEG~u@4sHzWnKGw&VO^YQsJXp*I&%u|LmHf&u?74>K<*+DyzQPuBw|p%#FU= zJ9eRMQ+xYpTW9{Qi@iU#jAlAljiJ%4V;9?`{5oaLhK(&F&CY$=UkI|zW4Pr$JD~P2X0;%KHGcz+(W}>3&$6HeR)ai!p)m+Ztl8%t>e4( zonJk2vu)AVq2{I)zZ5R5Xu5r-rFB(P>)VADms*={FKb@D?9`=Gzs=9xH~+}TN5nr{ zl!M|Ad-eDBzW>v=?|jj4OB@kL8rvT{(fFmlx54Wj()WD-Jgq$ZDE(yNtDpY6A-DAM k$)9`v*e*XI_q=lQ$_wjDOWWrzntS?2L&IM$_tg^lA2i{hn*aa+ diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/fb416f68-89e7-49e5-8303-1c604053a6e7-m0.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/fb416f68-89e7-49e5-8303-1c604053a6e7-m0.avro deleted file mode 100644 index 5dd5b94a8dff1c9aa7cae871c2941d6d30c68a51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4508 zcmb`K-)j_C6vvH55n6oExNcG(E<=oh8`+t$CfPzkXakaJOV-*NS*CYq?j~bqXQp%S zB&-rBr6@E|+FDahU)}iXQ!DnxK($Jte?VW_S{q~crAjUI7xdganc105%r6(hvbj6= zd(M2$k9+Rx#nUa@o~QZnP96DZPlH!OPI+_$6iUGge@8=sF4CCn67d2 z!Dw@tFl=CkwbP)nvipUM6yWLR0J0h2Kx}3L7_VE>N*f;YoCCUJ5Ph3LN)~2ho9@^QtZe0ero``o4 z-SL1wih_h7Ny<5;7SZUk+6ULGEi!`}XmD1k z3$8W<3qfoUlGT`RA_|Wpl?=IEH0AbNA~2pTup2W zCuLX$o}5+q>x6{g2ul?4KqKb67Rdy!jZqO(Vjw`mMhIse6f@$*oJ10oo~AoK6cLDr zeE?!8XJcs{6iMCsF;ND{DWO1tA~P_?87EB$>eM_#G}G3K6-F9sSTxKI!;yYNq9bnEnieOr?M-;Us=`X@5BdBSeK)`+gKGi|cl1C5bjJhco&m+fZ|2GFb8x9sQd~fmi};aO zU{-ZKLiwc^j%f-e;3@(p_(cUKWs?e5f{`km_&gZ8MQp-_GPcbUp-clAuOdxssQmQu z`}2)9LT!Dw(@7^srVtpSqgbS=-h2 z>)t)pRJCjPv$opTSEj0S_jg@MJ^Z|FPuKA9T=m;J~==1 z_RMr`8a{p-O23ei5mPTQ;A&aW4DcKh)N2>5R{6D7_KvS-N~6d)689eD2q%| z5RCpP4J0fH6eUd1lu*KY$il!N%!(i=L3*hm2#xOSI@{>zTCewh=bZ05-%oMbsa3WV z4FN?9$k-w9nQkxRb@~{emv%dIy*qs_50jw)6(Z~~hcS%X<#l^qG)G_c@(WPpHS%`i426Ic`jr3@{7}QJ0&pM_MDizL9-1a7gGa-vfXej{ zUgmV-BAEl2WMe;4QfRLdaKL_`)E?4@RBe`h2@MgV@+DN9CKXX88Vk_GGc4#jtfoMY zC7Bte7+JbZcmd&sa85>;c)l3q+!+mIG=L2^zykJ|z}Ach!sQseQ5gWwL!LFp zWT~2S{{in8M5wWx9JH2CyG?kDXRun(Flja$jlt}Jg@K16#H5611ZE2?1YrkMxoVjL zoaUxmTM(%r3PW@YjS8X=m_4+n5Sd$T@%YfD**ig@peT}vzY7ag71Zb$;-RFUi}___ z$diDfq;K%w=;-UMPd^ZlM#eCt621Wjqt7%1WwI9J(O$Xs4df{Bej> zK^ZhB$tO8XAShyzuCBgdsYSU8s%#kN9G?`bs^(K zsver5YHL@}u7EYH1To+N1{SMKM*pXH6s-OoIgsG#`nnjx`aqPK2sM{s|A)to=&6e*)o0ObaY3K z2)@4mL>2lU4vmlXf4_gT>FeIsj6G9x$L3OVv+H}~9yuEOo+KrdBpnT87r}(-+5X?> aI*TV}-rVl`a(SxZ=kTwb#-aMfRsR7}=X+!T diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-5701652792860281111-0-d85c7ea8-2fa8-4a02-9d11-6aca1be81ea1.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-5701652792860281111-0-d85c7ea8-2fa8-4a02-9d11-6aca1be81ea1.avro deleted file mode 100644 index 6630e2b198b7f28ee1f32b3e4e4c06572d049549..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1948 zcmb7ETSyd97%sDdl&+FxVapI#kX5ElL;45+gF3&d!`|M`zBgGiSBUOw1lC zq=$l_Hlm^^x=55Pf<;+LSHXy=pqmgS(ZzZ&uWGj|ecF9DY(cFO)z-Viko0 zK1gL*CT^W zu_QCS6g^9;0WTojAI`}L6VC^Nn9OJ(y#Z`*1599>0c_4#5=gFnEgXu$9+d&`Jmgt@ zOa`kt{U7iiL4+#H$$oSBbcO+M@(gYfR7{%9Mq@B~U}E5*2r(%k8iCOQ6G5*7id;EF z0gSQf<`zUMh{6!vLcM}01V#_dDMaR09XLL;VfKnCOyqJV&gAs$NFx#(L) z2YC`Ol(Y^06CHiMwdn@}Qpp&GReEoL&ge7@fm@bE2#|Ssdg0^`2@YS7J~X4{({~&q zRZu!j3HeD56Yz_eq^qg-TWV3RgbM2>qiI@a#&C2Dya5bV>MyK*pKnN3!;$$1R!vAh zk&2CWP}S9c(Jq4lR)QGt00Wa%2BZH|JaV74O`@uYxl)3&PuD%!bmVrZDKxe9?5N|h zZS7Z<-CQ(&bk~%9-pp7 z#4VpT;aq*nDfuWJe`ChaF76Fjy|!ub?2@mY*&jBP#b-6{fAO&Bu=`O(r}xt}bf7Ko zz>;wXrC{=1&+YQj?-~}by7#Q=eyFA6#PcsrDeXyH=ie!P-*IVKVtxY`w0NmKVCkkE zC!B9xwIfl18oRPI6kqsw%pH#H~c)xSPdaHun35>;5g zd+$0|Q_+fxju_j`mp#GUoTRoF*tSebao(n{^gqH%iDiEs2O&3->rcj{RW=Lm8t*$ diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-8842646200136284049-0-7e31226c-e1c9-4c2b-8a42-09812bdcba85.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-8842646200136284049-0-7e31226c-e1c9-4c2b-8a42-09812bdcba85.avro deleted file mode 100644 index c7db66df011c6df6e33e89af87500a60f52433ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1826 zcmb7ET}TvB7+p8}F)Sj&OeB^`BE+?sb$46Kf-H-&5VQ?JV!S(Z*BzU`=H7K(ZBeL) zFtC@R5DI!xAu6JJ3X^;&ED3rDG~bFasi1nXp6br-&U$rpwAXvTbI$jj@24Tso>y3e zRG_1nhP{%wI~4N!g8rb7Wx1-LFXU(a)nz&`po+az*akH&%R3Rg5QnM&y=q*EK*Pt!Q-UEl9^SCm8FluODbLn z7c>4)aWDF?Y!RR^aDvn zWDL_PnHyj+R#QU|(KHzX)xJC#IQb#L=?juYb7nr-fw z%US{&e1wdqZJjyl=uo^#3=y*eYh2ftWu=bHDp*Y+>qHs>HpHY-D`=mGNnU{%hyVkd zRg}^HDW1E}<|Z*I*Kk*XJn&=i&zt@!`Qn|=hujnHgI6DI8e_NAU7F)=-f|TLB5T^e zek(0w`gcv|pS@cbag|K=_U)K-zuUTp8TmN6cVzwanSj^T`h7eZyB+Q;o%MA#Hy(eg zj33sA6XC}<)~>t$A((e>@bB!E!TaM!!krDjdizg4oAc+lvR@iYHvX;$#b}_ccEUe& zbf)Hg?a#ud7oTeCFPA?VlV6{%dHHr~y1cLWz-Z;r%uH9$wdRLC&!ewu7bpG?=owo! diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-3899460b-fb8e-4c6e-9d72-b475830bc2bb.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-3899460b-fb8e-4c6e-9d72-b475830bc2bb.metadata.json deleted file mode 100644 index beb2e59..0000000 --- a/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-3899460b-fb8e-4c6e-9d72-b475830bc2bb.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"ca55ff11-a737-469e-9bf7-30c958b8f30f","last-updated-ms":1775656363842,"last-column-id":12,"schemas":[{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":true},{"id":4,"name":"frame_id","type":"long","required":true},{"id":5,"name":"period","type":"string","required":true},{"id":6,"name":"timestamp","type":"timestamp","required":true},{"id":7,"name":"entity_type","type":"string","required":true},{"id":8,"name":"entity_id","type":"string","required":true},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[{"source-id":3,"field-id":1000,"transform":"identity","name":"match_id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"snapshots":[],"snapshot-log":[],"metadata-log":[],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":0} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-0203e49b-a178-4260-a16a-3dc465003124.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-0203e49b-a178-4260-a16a-3dc465003124.metadata.json deleted file mode 100644 index 6391a18..0000000 --- a/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-0203e49b-a178-4260-a16a-3dc465003124.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"ca55ff11-a737-469e-9bf7-30c958b8f30f","last-updated-ms":1775656370198,"last-column-id":15,"schemas":[{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":true},{"id":4,"name":"frame_id","type":"long","required":true},{"id":5,"name":"period","type":"string","required":true},{"id":6,"name":"timestamp","type":"timestamp","required":true},{"id":7,"name":"entity_type","type":"string","required":true},{"id":8,"name":"entity_id","type":"string","required":true},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":false},{"id":4,"name":"frame_id","type":"long","required":false},{"id":5,"name":"period","type":"string","required":false},{"id":6,"name":"timestamp","type":"timestamp","required":false},{"id":7,"name":"entity_type","type":"string","required":false},{"id":8,"name":"entity_id","type":"string","required":false},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false},{"id":13,"name":"acc","type":"double","required":false},{"id":14,"name":"vel","type":"double","required":false},{"id":15,"name":"entity_key","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":3,"field-id":1000,"transform":"identity","name":"match_id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":1282166195388124131,"snapshots":[{"snapshot-id":1282166195388124131,"sequence-number":1,"timestamp-ms":1775656370198,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-1282166195388124131-0-fc9a80a6-f0eb-4290-8070-14be9d80f7e7.avro","summary":{"operation":"append","added-files-size":"42692748","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"42692748","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":1282166195388124131,"timestamp-ms":1775656370198}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-3899460b-fb8e-4c6e-9d72-b475830bc2bb.metadata.json","timestamp-ms":1775656363842}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":1282166195388124131,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":1} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-ec4a76bd-8c50-41ea-b5cc-28d517cd4616.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-ec4a76bd-8c50-41ea-b5cc-28d517cd4616.metadata.json deleted file mode 100644 index 4921cbb..0000000 --- a/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-ec4a76bd-8c50-41ea-b5cc-28d517cd4616.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"ca55ff11-a737-469e-9bf7-30c958b8f30f","last-updated-ms":1775656376225,"last-column-id":15,"schemas":[{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":true},{"id":4,"name":"frame_id","type":"long","required":true},{"id":5,"name":"period","type":"string","required":true},{"id":6,"name":"timestamp","type":"timestamp","required":true},{"id":7,"name":"entity_type","type":"string","required":true},{"id":8,"name":"entity_id","type":"string","required":true},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":false},{"id":4,"name":"frame_id","type":"long","required":false},{"id":5,"name":"period","type":"string","required":false},{"id":6,"name":"timestamp","type":"timestamp","required":false},{"id":7,"name":"entity_type","type":"string","required":false},{"id":8,"name":"entity_id","type":"string","required":false},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false},{"id":13,"name":"acc","type":"double","required":false},{"id":14,"name":"vel","type":"double","required":false},{"id":15,"name":"entity_key","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":3,"field-id":1000,"transform":"identity","name":"match_id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":5997955711447099734,"snapshots":[{"snapshot-id":1282166195388124131,"sequence-number":1,"timestamp-ms":1775656370198,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-1282166195388124131-0-fc9a80a6-f0eb-4290-8070-14be9d80f7e7.avro","summary":{"operation":"append","added-files-size":"42692748","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"42692748","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":5997955711447099734,"parent-snapshot-id":1282166195388124131,"sequence-number":2,"timestamp-ms":1775656376225,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-5997955711447099734-0-ecfdfc8e-3ec5-4fb9-b6c2-5b5b776d94bb.avro","summary":{"operation":"append","added-files-size":"39675605","added-data-files":"2","added-records":"3832763","changed-partition-count":"1","total-data-files":"4","total-delete-files":"0","total-records":"7919839","total-files-size":"82368353","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":1282166195388124131,"timestamp-ms":1775656370198},{"snapshot-id":5997955711447099734,"timestamp-ms":1775656376225}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-3899460b-fb8e-4c6e-9d72-b475830bc2bb.metadata.json","timestamp-ms":1775656363842},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-0203e49b-a178-4260-a16a-3dc465003124.metadata.json","timestamp-ms":1775656370198}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":5997955711447099734,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":2} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-1ebc3653-47d1-4eb3-8895-324002808f47.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-1ebc3653-47d1-4eb3-8895-324002808f47.metadata.json deleted file mode 100644 index e3278d1..0000000 --- a/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-1ebc3653-47d1-4eb3-8895-324002808f47.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"ca55ff11-a737-469e-9bf7-30c958b8f30f","last-updated-ms":1775656381686,"last-column-id":15,"schemas":[{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":true},{"id":4,"name":"frame_id","type":"long","required":true},{"id":5,"name":"period","type":"string","required":true},{"id":6,"name":"timestamp","type":"timestamp","required":true},{"id":7,"name":"entity_type","type":"string","required":true},{"id":8,"name":"entity_id","type":"string","required":true},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":false},{"id":4,"name":"frame_id","type":"long","required":false},{"id":5,"name":"period","type":"string","required":false},{"id":6,"name":"timestamp","type":"timestamp","required":false},{"id":7,"name":"entity_type","type":"string","required":false},{"id":8,"name":"entity_id","type":"string","required":false},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false},{"id":13,"name":"acc","type":"double","required":false},{"id":14,"name":"vel","type":"double","required":false},{"id":15,"name":"entity_key","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":3,"field-id":1000,"transform":"identity","name":"match_id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":601469116504892914,"snapshots":[{"snapshot-id":1282166195388124131,"sequence-number":1,"timestamp-ms":1775656370198,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-1282166195388124131-0-fc9a80a6-f0eb-4290-8070-14be9d80f7e7.avro","summary":{"operation":"append","added-files-size":"42692748","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"42692748","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":5997955711447099734,"parent-snapshot-id":1282166195388124131,"sequence-number":2,"timestamp-ms":1775656376225,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-5997955711447099734-0-ecfdfc8e-3ec5-4fb9-b6c2-5b5b776d94bb.avro","summary":{"operation":"append","added-files-size":"39675605","added-data-files":"2","added-records":"3832763","changed-partition-count":"1","total-data-files":"4","total-delete-files":"0","total-records":"7919839","total-files-size":"82368353","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":601469116504892914,"parent-snapshot-id":5997955711447099734,"sequence-number":3,"timestamp-ms":1775656381686,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-601469116504892914-0-f58e70eb-6eb2-4a19-9b08-5b01b851d469.avro","summary":{"operation":"append","added-files-size":"35605962","added-data-files":"1","added-records":"3430350","changed-partition-count":"1","total-data-files":"5","total-delete-files":"0","total-records":"11350189","total-files-size":"117974315","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":1282166195388124131,"timestamp-ms":1775656370198},{"snapshot-id":5997955711447099734,"timestamp-ms":1775656376225},{"snapshot-id":601469116504892914,"timestamp-ms":1775656381686}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-3899460b-fb8e-4c6e-9d72-b475830bc2bb.metadata.json","timestamp-ms":1775656363842},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-0203e49b-a178-4260-a16a-3dc465003124.metadata.json","timestamp-ms":1775656370198},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-ec4a76bd-8c50-41ea-b5cc-28d517cd4616.metadata.json","timestamp-ms":1775656376225}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":601469116504892914,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":3} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/ecfdfc8e-3ec5-4fb9-b6c2-5b5b776d94bb-m0.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/ecfdfc8e-3ec5-4fb9-b6c2-5b5b776d94bb-m0.avro deleted file mode 100644 index e97b02e70eccb54d5c556c9fc6b186505b7bf6aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6113 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 diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/f58e70eb-6eb2-4a19-9b08-5b01b851d469-m0.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/f58e70eb-6eb2-4a19-9b08-5b01b851d469-m0.avro deleted file mode 100644 index a3d91abd4a73f5922e7c248f1f72ecae45135688..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5589 zcmb_gYiu0V6}G()I}I{zt%uN!3nG(HUbl(MDFv0z>2*5aNs{>M(gzr$S0zj%qZ?X^HyA$?7l|b3OyiaZ z7*SvvLw-iaN%{t_BvoRFhN#FKeB#fr6y>e(zec)KqDH_djwPL>{gwyT@t_l|PtiE+ z7$>p8cTOpAjbj_v;0PXDNy(LRtz)SYO;o0E*gD735R*hVa8h-V%6v2dSoMxQkpb&o z&MeQQv2MsVpGu)3j-(gPQCN6s3Df{yJk-SfsJ1=E(A;H)9Hw|XaUd;A~`a2Td0RfQ?PRzXtaz8l5xZd(QNlK7BK^()Ds1250umbCNJSRrDl7& zQpK9Z#el97{+U9ctFELpo|vX^Ui*EZa4-nvF*GdeaYai4l`XdhO=*?kh^7Poi`eYa*VcI&89%Pi!YZDHh; z1m#1FN&9yA19HYG$zU20{mAPQWQ@o>;p9{@MzpMK!qasd7bU{!Iephm*EdC+qXDv# zWnG7pEx~9JvmAxO8M&ENpcpDV;K!_oxX9`cL<`x7Xzl$-(Rlg`K|2AUS#>kfP_faO z9SB<0Ew@b+C)QmGkLdtH_uUgk+eD)it-lwUT}HfpVfwd{8WSO-Gw9eeh0TdDtz8sK)57ECW{?t+?F0a!8lD#AKA>(LP3@yWEbsjTRsr&9b}|biTEufF^XZG z2nbL%IteYGB{YZ%jTdE$%^a_!fS1|nW-!I#og2NyWEN85$nu~g(F#V(<_%#MQWv+w z_?Ed?uR*fqAr#DPO>3?@$81v!EFtOxQ6Dsn<-@~xUXfBsnWJxv_1*w!kIIh&x=SXk zX&7V_l*u4TkY;1(~o zbW(A-0ER6rj{vhF3NSl;fZ5PA0A}+CFvSa*ctHu_7;re`n8EybD!;$HO z8E)`oKPlpeBl!fAE<;ogpf4>{9o4oJ!V9mYY$F;M1KeR_1tc>CiLK2iv}_&i(|RY! zlr@#D%MI!s=0}Rdt|#uF-eJeOQ@@oXmCnA zt`lPn5yBbkp%dQSAe`lbNF)h2D?4erS=sxBwOZwzN2aFSHb%CK%~8Lp?_Il+dZqZHjdVb2hVLA0bO6Qn8Ikewmj-<-b%h-tvh1C~3z zItz3~1I#r6$V0yX@v2YFcvI5cwQ^NF+623*su7)D$YD(D5CU#D5Q5%)U{e-#GZky4 zvNt{rmTt2)_Jbsr#W;K~#DN>Fmq{0VcR7A?^D{3jUAB8<=V@-_YSGyeKDM1rcV9f4 zR^$@6y8jn9pC69??!${0uMe~YZuofhiuYQZn*+@oF05Gj@wRB-&iCJ4eMjKK)hnCt zx^Q9m&ZkQ_~ulihi5f?-B+?^_L2f-*R8us_RRLpnTyu` zr0Jc$lF00bx)%A0eG7fp`xfsk*!hQNuNE=4{A<-?g)g3Y_=kCikM~_yfe!K=9kGt5 zW4HZt(W8BRjlObq?;t5#FmPsT)s?|-N-8e}hnl{Rtnw|s{hnA$f5E1Kw+;+cuV{Pn z=eru_o}agBT|;^KXQg|)zg;}{#;QUgTyw|F2S3{L*IT~5Z=~;uf-`NqXGE@Td!x2u z)35h`^1(ITyKn7Yzhg&qhdA@8g4564c)8~7{uR*;!wY5??cZ4OpOYunR-(DmTL;R| zoxUNod}jZlk(K@G)t?I2Z03r;+V|SgzoX^8iyM7R-=}Z?zWb}zh4){6PdT;vPya6R O?Jg|1a^Nr7UGqP)#UAPa diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/fc9a80a6-f0eb-4290-8070-14be9d80f7e7-m0.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/fc9a80a6-f0eb-4290-8070-14be9d80f7e7-m0.avro deleted file mode 100644 index 003e84567eebcd503d13a2bd783e04617c469e57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6122 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 Q<|UfiKVf0-xslfV0gVrng#Z8m diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-601469116504892914-0-f58e70eb-6eb2-4a19-9b08-5b01b851d469.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-601469116504892914-0-f58e70eb-6eb2-4a19-9b08-5b01b851d469.avro deleted file mode 100644 index 63e44a5d71cd903af670ed5dc8febfef598d13b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2001 zcmeZI%3|aZE6z(SD9*?)(alUzF*7hUF|#x@G&40Yv9L6Mt1YHny~Vq$IpgJW^CZ!fJ8Ox;Q7v(0F=$54x6=&w>F&T*@ zmKEjeCFiH4CiA7FrsX7-q>6!sijy-^a}(EzS1XlN7Nja!DJT`CCg&HWDCsCDrDdk( zq!a_iqN|ni5_7?_xruq1X{p5}@db$`8E{2F=L40Q8W;d|!OSTxDay=C2PsL(PX-z6 zlb@Ve0(6Z+XppBud1gt5f?Kcx*#1;$g`Io1cw|(4Ur>lUf{~oL`FReFGCD{TN}ET9H{?0*vf@ zo_q|D(q6PuUTIDaPz55xCKeSXR)X{Z!!Q?ATtG9U1yCrnBsCY5gAuCnXKzsIPt43K z1|~zWEs!z*W}+ohB1p>5&q+ zv-Pnay=d#r>?pwY;3Ln$qUcXcQzO~FrK+s-Fwe0x2duuFxiXOh}xi+m#ENSwo z1+5&D;&fFy5>_sl+q*QPBPy~&P)Xuad*(CQithH8QdVmo@hmOTJ!rA8*J6@Ta>!JZ zg{nD8rdbI-mm^l3-LUT=yOQX>u2(BLUf+~0cUc>Fw582)(t`cENo!o9Isb|NGtcA= z{_a<7Sqn<;;W;jW-y;7bQ>eow)vzB;;=6Z^DMT%vca6E5&R zW%uzG`-!LL`fkr(+kN-+uVqR {tid} > {attr} + # instead of teams > {tid} > attributes > {attr} entities_hierarchical: dict[str, Any] = {"ball": {}, "teams": {}, "referees": {}} for ent in entities_flat: etype = ent.pop("entity_type") @@ -375,14 +377,15 @@ def _save_tracking_data( entities_hierarchical["referees"][eid] = attrs elif etype == "player": if tid not in entities_hierarchical["teams"]: - entities_hierarchical["teams"][tid] = {"players": {}, "attributes": {}} + entities_hierarchical["teams"][tid] = {"players": {}} entities_hierarchical["teams"][tid]["players"][eid] = attrs elif etype == "team": # Use tid (e.g. 'home') instead of eid (e.g. 'team:home') for the key target_tid = tid if tid != "none" else eid.replace("team:", "") if target_tid not in entities_hierarchical["teams"]: - entities_hierarchical["teams"][target_tid] = {"players": {}, "attributes": {}} - entities_hierarchical["teams"][target_tid]["attributes"] = attrs + entities_hierarchical["teams"][target_tid] = {"players": {}} + # Merge team-level attributes directly into the team object + entities_hierarchical["teams"][target_tid].update(attrs) if columns_to_drop: logger.info(f"Normalizing constant attributes to metadata: {columns_to_drop}") @@ -514,9 +517,9 @@ def _get_flat_entities(self, hierarchical_entities: dict[str, Any]) -> list[dict # 2. Teams & Players for tid, team_info in hierarchical_entities.get("teams", {}).items(): # Team itself - flat.append( - {"entity_id": f"team:{tid}", "team_id": tid, **team_info.get("attributes", {})} - ) + # We need to exclude "players" from the attributes when flattening the team itself + team_attrs = {k: v for k, v in team_info.items() if k != "players"} + flat.append({"entity_id": f"team:{tid}", "team_id": tid, **team_attrs}) # Players for pid, p_attrs in team_info.get("players", {}).items(): flat.append({"entity_id": pid, "team_id": tid, **p_attrs}) From 6c9cf8e398399236c250bb03540980d5eb0424b2 Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Wed, 8 Apr 2026 16:08:53 +0200 Subject: [PATCH 17/21] fix: remove redundant team/id for referees and exclude entity_key from wide format --- crates/tdc-core/src/datacontainer/core.rs | 43 ++++++---- crates/tdc-core/src/schema/entity.rs | 18 +++++ examples/08-idsse-warehouse.ipynb | 99 +++++++++++------------ python/tracking_data_container/catalog.py | 20 +++-- 4 files changed, 105 insertions(+), 75 deletions(-) diff --git a/crates/tdc-core/src/datacontainer/core.rs b/crates/tdc-core/src/datacontainer/core.rs index 7ba684b..9b1a3ce 100644 --- a/crates/tdc-core/src/datacontainer/core.rs +++ b/crates/tdc-core/src/datacontainer/core.rs @@ -424,7 +424,7 @@ impl TrackingDataContainer { attr_indices.push(i); // Also pivot it } else if core_names.contains(name) { core_indices.push(i); - } else if name == "entity_type" { + } else if name == "entity_type" || name == "entity_key" { continue; } else { attr_indices.push(i); @@ -635,10 +635,10 @@ impl TrackingDataContainer { } }; - let attr_builder_indices = entity_map.entry((p_id, t_id)).or_insert_with(|| { + let attr_mappings = entity_map.entry((p_id, t_id)).or_insert_with(|| { attr_indices .iter() - .map(|&attr_idx| { + .filter_map(|&attr_idx| { let attr_name = fields[attr_idx].name(); let entity = if p_id == "ball" { TrackedEntity::Ball @@ -658,12 +658,21 @@ impl TrackingDataContainer { }; let attribute = match attr_name.as_str() { - "entity_id" => crate::schema::TrackedEntityAttribute::Other( - "player/id".to_string(), - ), + "entity_id" => { + if entity.is_ball() { + return None; + } + crate::schema::TrackedEntityAttribute::Other( + "player/id".to_string(), + ) + } "team_id" => { + if !entity.is_player() && !entity.is_team() { + return None; + } crate::schema::TrackedEntityAttribute::Other("team/id".to_string()) } + "entity_key" => return None, _ => crate::schema::TrackedEntityAttribute::from_str(attr_name) .unwrap_or_else(|_| { crate::schema::TrackedEntityAttribute::Other( @@ -673,47 +682,47 @@ impl TrackingDataContainer { }; let field = DataField::TrackedEntity { entity, attribute }; - builder.get_or_create_column_id( + let builder_idx = builder.get_or_create_column_id( &field.name(), fields[attr_idx].data_type().clone(), - ) + ); + Some((attr_idx, builder_idx)) }) .collect::>() }); - for (attr_pos, &builder_col_idx) in attr_builder_indices.iter().enumerate() { - let attr_idx = attr_indices[attr_pos]; - let col = &columns[attr_idx]; + for (attr_idx, builder_col_idx) in attr_mappings { + let col: &ArrayRef = &columns[*attr_idx]; if !col.is_null(i) { match col.data_type() { Float32 => builder.insert_float_at_idx( row_idx, - builder_col_idx, + *builder_col_idx, col.as_primitive::().value(i) as f64, ), Float64 => builder.insert_float_at_idx( row_idx, - builder_col_idx, + *builder_col_idx, col.as_primitive::().value(i), ), Int64 => builder.insert_int_at_idx( row_idx, - builder_col_idx, + *builder_col_idx, col.as_primitive::().value(i), ), UInt64 => builder.insert_int_at_idx( row_idx, - builder_col_idx, + *builder_col_idx, col.as_primitive::().value(i) as i64, ), Utf8 => builder.insert_string_at_idx( row_idx, - builder_col_idx, + *builder_col_idx, col.as_string::().value(i), ), LargeUtf8 => builder.insert_string_at_idx( row_idx, - builder_col_idx, + *builder_col_idx, col.as_string::().value(i), ), _ => {} diff --git a/crates/tdc-core/src/schema/entity.rs b/crates/tdc-core/src/schema/entity.rs index 76d4b36..cf50b34 100644 --- a/crates/tdc-core/src/schema/entity.rs +++ b/crates/tdc-core/src/schema/entity.rs @@ -26,3 +26,21 @@ pub enum TrackedEntity { team_id: String, }, } + +impl TrackedEntity { + pub fn is_ball(&self) -> bool { + matches!(self, TrackedEntity::Ball) + } + + pub fn is_player(&self) -> bool { + matches!(self, TrackedEntity::Player { .. }) + } + + pub fn is_team(&self) -> bool { + matches!(self, TrackedEntity::Team { .. }) + } + + pub fn is_referee(&self) -> bool { + matches!(self, TrackedEntity::Referee { .. }) + } +} diff --git a/examples/08-idsse-warehouse.ipynb b/examples/08-idsse-warehouse.ipynb index e8e7ed8..710a801 100644 --- a/examples/08-idsse-warehouse.ipynb +++ b/examples/08-idsse-warehouse.ipynb @@ -138,15 +138,15 @@ "text": [ "\n", "Processing match DFL-MAT-J03WMX...\n", - " ✅ Parsed in 1.54s, Saved in 4.82s\n", + " ✅ Parsed in 1.94s, Saved in 5.75s\n", " ✅ Rows: 145,967\n", "\n", "Processing match DFL-MAT-J03WN1...\n", - " ✅ Parsed in 1.38s, Saved in 4.65s\n", + " ✅ Parsed in 1.83s, Saved in 5.09s\n", " ✅ Rows: 141,561\n", "\n", "Processing match DFL-MAT-J03WOH...\n", - " ✅ Parsed in 1.28s, Saved in 4.18s\n", + " ✅ Parsed in 1.46s, Saved in 4.33s\n", " ✅ Rows: 137,214\n" ] } @@ -242,7 +242,7 @@ "output_type": "stream", "text": [ "Loading data for DFL-MAT-J03WMX...\n", - " ✅ Loaded and pivoted 165259 rows in 1.4731s\n" + " ✅ Loaded and pivoted 165259 rows in 1.5354s\n" ] }, { @@ -255,28 +255,27 @@ " white-space: pre-wrap;\n", "}\n", "\n", - "shape: (5, 316)
competition/idseason/idmatch/idframe_idperiodtimestampteams/away/players/DFL-OBJ-J017RE/player/idteams/away/players/DFL-OBJ-J017RE/team/idteams/away/players/DFL-OBJ-J017RE/xteams/away/players/DFL-OBJ-J017RE/yteams/away/players/DFL-OBJ-J017RE/zteams/away/players/DFL-OBJ-J017RE/accteams/away/players/DFL-OBJ-J017RE/velteams/away/players/DFL-OBJ-J017RE/entity_keyteams/away/players/DFL-OBJ-J01APO/player/idteams/away/players/DFL-OBJ-J01APO/team/idteams/away/players/DFL-OBJ-J01APO/xteams/away/players/DFL-OBJ-J01APO/yteams/away/players/DFL-OBJ-J01APO/zteams/away/players/DFL-OBJ-J01APO/accteams/away/players/DFL-OBJ-J01APO/velteams/away/players/DFL-OBJ-J01APO/entity_keyteams/home/players/DFL-OBJ-002GKO/player/idteams/home/players/DFL-OBJ-002GKO/team/idteams/home/players/DFL-OBJ-002GKO/xteams/home/players/DFL-OBJ-002GKO/yteams/home/players/DFL-OBJ-002GKO/zteams/home/players/DFL-OBJ-002GKO/accteams/home/players/DFL-OBJ-002GKO/velteams/home/players/DFL-OBJ-002GKO/entity_keyteams/away/players/DFL-OBJ-0000RP/player/idteams/away/players/DFL-OBJ-0000RP/team/idteams/away/players/DFL-OBJ-0000RP/xteams/away/players/DFL-OBJ-0000RP/yteams/away/players/DFL-OBJ-0000RP/zteams/away/players/DFL-OBJ-0000RP/accteams/away/players/DFL-OBJ-0000RP/velteams/home/players/DFL-OBJ-002GBW/team/idteams/home/players/DFL-OBJ-002GBW/xteams/home/players/DFL-OBJ-002GBW/yteams/home/players/DFL-OBJ-002GBW/zteams/home/players/DFL-OBJ-002GBW/accteams/home/players/DFL-OBJ-002GBW/velteams/home/players/DFL-OBJ-002GBW/entity_keyteams/home/players/DFL-OBJ-002G4A/player/idteams/home/players/DFL-OBJ-002G4A/team/idteams/home/players/DFL-OBJ-002G4A/xteams/home/players/DFL-OBJ-002G4A/yteams/home/players/DFL-OBJ-002G4A/zteams/home/players/DFL-OBJ-002G4A/accteams/home/players/DFL-OBJ-002G4A/velteams/home/players/DFL-OBJ-002G4A/entity_keyteams/away/players/DFL-OBJ-0027G6/player/idteams/away/players/DFL-OBJ-0027G6/team/idteams/away/players/DFL-OBJ-0027G6/xteams/away/players/DFL-OBJ-0027G6/yteams/away/players/DFL-OBJ-0027G6/zteams/away/players/DFL-OBJ-0027G6/accteams/away/players/DFL-OBJ-0027G6/velteams/away/players/DFL-OBJ-0027G6/entity_keyteams/home/players/DFL-OBJ-0027V2/player/idteams/home/players/DFL-OBJ-0027V2/team/idteams/home/players/DFL-OBJ-0027V2/xteams/home/players/DFL-OBJ-0027V2/yteams/home/players/DFL-OBJ-0027V2/zteams/home/players/DFL-OBJ-0027V2/accteams/home/players/DFL-OBJ-0027V2/velteams/home/players/DFL-OBJ-0027V2/entity_keyteams/team:home/idteams/team:home/nameteams/team:home/jersey_colourteams/team:away/idteams/team:away/nameteams/team:away/jersey_colour
strstrstri64strdatetime[μs]strstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64strf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrstrstrstrstr
nullnull"DFL-MAT-J03WMX"10000"first_half"2023-05-27 13:30:12.600"DFL-OBJ-J017RE""away"-20.779.07null0.00.0"DFL-OBJ-J017RE|away""DFL-OBJ-J01APO""away"-13.825.719999null0.00.0"DFL-OBJ-J01APO|away""DFL-OBJ-002GKO""home"11.375.18null0.00.0"DFL-OBJ-002GKO|home"nullnullnullnullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-002G4A""home"10.39-3.39null0.00.0"DFL-OBJ-002G4A|home""DFL-OBJ-0027G6""away"0.78-0.54null0.00.0"DFL-OBJ-0027G6|away"nullnullnullnullnullnullnullnull"DFL-CLU-000008""1. FC Köln""#F4F5F0""DFL-CLU-00000G""FC Bayern München""#25282A"
nullnull"DFL-MAT-J03WMX"10001"first_half"2023-05-27 13:30:12.640"DFL-OBJ-J017RE""away"-20.779.11null2.113.1"DFL-OBJ-J017RE|away""DFL-OBJ-J01APO""away"-13.7825.74null1.232.59"DFL-OBJ-J01APO|away""DFL-OBJ-002GKO""home"11.345.23null1.354.77"DFL-OBJ-002GKO|home"nullnullnullnullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-002G4A""home"10.28-3.39null1.2210.72"DFL-OBJ-002G4A|home""DFL-OBJ-0027G6""away"0.85-0.56null2.347.74"DFL-OBJ-0027G6|away"nullnullnullnullnullnullnullnull"DFL-CLU-000008""1. FC Köln""#F4F5F0""DFL-CLU-00000G""FC Bayern München""#25282A"
nullnull"DFL-MAT-J03WMX"10002"first_half"2023-05-27 13:30:12.680"DFL-OBJ-J017RE""away"-20.779.14null2.063.4"DFL-OBJ-J017RE|away""DFL-OBJ-J01APO""away"-13.7525.76null1.082.76"DFL-OBJ-J01APO|away""DFL-OBJ-002GKO""home"11.315.28null1.264.95"DFL-OBJ-002GKO|home"nullnullnullnullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-002G4A""home"10.16-3.39null1.1310.9"DFL-OBJ-002G4A|home""DFL-OBJ-0027G6""away"0.94-0.59null2.288.08"DFL-OBJ-0027G6|away"nullnullnullnullnullnullnullnull"DFL-CLU-000008""1. FC Köln""#F4F5F0""DFL-CLU-00000G""FC Bayern München""#25282A"
nullnull"DFL-MAT-J03WMX"10003"first_half"2023-05-27 13:30:12.720"DFL-OBJ-J017RE""away"-20.779.19null2.03.7"DFL-OBJ-J017RE|away""DFL-OBJ-J01APO""away"-13.7325.780001null0.912.89"DFL-OBJ-J01APO|away""DFL-OBJ-002GKO""home"11.295.33null1.135.12"DFL-OBJ-002GKO|home"nullnullnullnullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-002G4A""home"10.03-3.39null1.0311.06"DFL-OBJ-002G4A|home""DFL-OBJ-0027G6""away"1.03-0.62null2.268.4"DFL-OBJ-0027G6|away"nullnullnullnullnullnullnullnull"DFL-CLU-000008""1. FC Köln""#F4F5F0""DFL-CLU-00000G""FC Bayern München""#25282A"
nullnull"DFL-MAT-J03WMX"10004"first_half"2023-05-27 13:30:12.760"DFL-OBJ-J017RE""away"-20.7800019.22null1.923.98"DFL-OBJ-J017RE|away""DFL-OBJ-J01APO""away"-13.7125.809999null0.723.0"DFL-OBJ-J01APO|away""DFL-OBJ-002GKO""home"11.265.38null0.985.28"DFL-OBJ-002GKO|home"nullnullnullnullnullnullnullnullnullnullnullnullnullnull"DFL-OBJ-002G4A""home"9.91-3.39null0.9211.19"DFL-OBJ-002G4A|home""DFL-OBJ-0027G6""away"1.12-0.65null2.298.74"DFL-OBJ-0027G6|away"nullnullnullnullnullnullnullnull"DFL-CLU-000008""1. FC Köln""#F4F5F0""DFL-CLU-00000G""FC Bayern München""#25282A"
" + "shape: (5, 316)
competition/idseason/idmatch/idframe_idperiodtimestampreferees/DFL-OBJ-00011J/player/idreferees/DFL-OBJ-00011J/team/idreferees/DFL-OBJ-00011J/xreferees/DFL-OBJ-00011J/yreferees/DFL-OBJ-00011J/zreferees/DFL-OBJ-00011J/velreferees/DFL-OBJ-00011J/accreferees/DFL-OBJ-00011J/entity_keyteams/home/players/DFL-OBJ-002GKO/player/idteams/home/players/DFL-OBJ-002GKO/team/idteams/home/players/DFL-OBJ-002GKO/xteams/home/players/DFL-OBJ-002GKO/yteams/home/players/DFL-OBJ-002GKO/zteams/home/players/DFL-OBJ-002GKO/velteams/home/players/DFL-OBJ-002GKO/accteams/home/players/DFL-OBJ-002GKO/entity_keyteams/away/players/DFL-OBJ-0002DR/player/idteams/away/players/DFL-OBJ-0002DR/team/idteams/away/players/DFL-OBJ-0002DR/xteams/away/players/DFL-OBJ-0002DR/yteams/away/players/DFL-OBJ-0002DR/zteams/away/players/DFL-OBJ-0002DR/velteams/away/players/DFL-OBJ-0002DR/accteams/away/players/DFL-OBJ-0002DR/entity_keyteams/home/players/DFL-OBJ-002GBW/player/idteams/home/players/DFL-OBJ-002GBW/team/idteams/home/players/DFL-OBJ-002GBW/xteams/home/players/DFL-OBJ-002GBW/yteams/home/players/DFL-OBJ-002GBW/zteams/home/players/DFL-OBJ-002GBW/velteams/home/players/DFL-OBJ-002GBW/accreferees/DFL-OBJ-00011W/team/idreferees/DFL-OBJ-00011W/xreferees/DFL-OBJ-00011W/yreferees/DFL-OBJ-00011W/zreferees/DFL-OBJ-00011W/velreferees/DFL-OBJ-00011W/accreferees/DFL-OBJ-00011W/entity_keyteams/away/players/DFL-OBJ-J01APO/player/idteams/away/players/DFL-OBJ-J01APO/team/idteams/away/players/DFL-OBJ-J01APO/xteams/away/players/DFL-OBJ-J01APO/yteams/away/players/DFL-OBJ-J01APO/zteams/away/players/DFL-OBJ-J01APO/velteams/away/players/DFL-OBJ-J01APO/accteams/away/players/DFL-OBJ-J01APO/entity_keyteams/away/players/DFL-OBJ-0026PM/player/idteams/away/players/DFL-OBJ-0026PM/team/idteams/away/players/DFL-OBJ-0026PM/xteams/away/players/DFL-OBJ-0026PM/yteams/away/players/DFL-OBJ-0026PM/zteams/away/players/DFL-OBJ-0026PM/velteams/away/players/DFL-OBJ-0026PM/accteams/away/players/DFL-OBJ-0026PM/entity_keyteams/home/players/DFL-OBJ-002G4A/player/idteams/home/players/DFL-OBJ-002G4A/team/idteams/home/players/DFL-OBJ-002G4A/xteams/home/players/DFL-OBJ-002G4A/yteams/home/players/DFL-OBJ-002G4A/zteams/home/players/DFL-OBJ-002G4A/velteams/home/players/DFL-OBJ-002G4A/accteams/home/players/DFL-OBJ-002G4A/entity_keyteams/away/idteams/away/jersey_colourteams/away/nameteams/home/idteams/home/jersey_colourteams/home/name
strstrstri64strdatetime[μs]strstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64strf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrstrstrstrstr
nullnull"DFL-MAT-J03WMX"10000"first_half"2023-05-27 13:30:12.600"referee:DFL-OBJ-00011J"null-20.4934.200001null0.00.0"referee:DFL-OBJ-00011J|none""DFL-OBJ-002GKO""home"11.375.18null0.00.0"DFL-OBJ-002GKO|home""DFL-OBJ-0002DR""away"-46.730.18null0.00.0"DFL-OBJ-0002DR|away"nullnullnullnullnullnullnullnull19.950001-34.450001null0.00.0"referee:DFL-OBJ-00011W|none""DFL-OBJ-J01APO""away"-13.825.719999null0.00.0"DFL-OBJ-J01APO|away""DFL-OBJ-0026PM""away"0.4233.220001null0.00.0"DFL-OBJ-0026PM|away""DFL-OBJ-002G4A""home"10.39-3.39null0.00.0"DFL-OBJ-002G4A|home""DFL-CLU-00000G""#25282A""FC Bayern München""DFL-CLU-000008""#F4F5F0""1. FC Köln"
nullnull"DFL-MAT-J03WMX"10001"first_half"2023-05-27 13:30:12.640"referee:DFL-OBJ-00011J"null-20.4934.200001null0.190.86"referee:DFL-OBJ-00011J|none""DFL-OBJ-002GKO""home"11.345.23null4.771.35"DFL-OBJ-002GKO|home""DFL-OBJ-0002DR""away"-46.7099990.18null2.211.01"DFL-OBJ-0002DR|away"nullnullnullnullnullnullnullnull19.92-34.450001null1.771.48"referee:DFL-OBJ-00011W|none""DFL-OBJ-J01APO""away"-13.7825.74null2.591.23"DFL-OBJ-J01APO|away""DFL-OBJ-0026PM""away"0.5933.18null12.60.0"DFL-OBJ-0026PM|away""DFL-OBJ-002G4A""home"10.28-3.39null10.721.22"DFL-OBJ-002G4A|home""DFL-CLU-00000G""#25282A""FC Bayern München""DFL-CLU-000008""#F4F5F0""1. FC Köln"
nullnull"DFL-MAT-J03WMX"10002"first_half"2023-05-27 13:30:12.680"referee:DFL-OBJ-00011J"null-20.4934.200001null0.320.99"referee:DFL-OBJ-00011J|none""DFL-OBJ-002GKO""home"11.315.28null4.951.26"DFL-OBJ-002GKO|home""DFL-OBJ-0002DR""away"-46.680.18null2.350.98"DFL-OBJ-0002DR|away"nullnullnullnullnullnullnullnull19.91-34.450001null1.981.42"referee:DFL-OBJ-00011W|none""DFL-OBJ-J01APO""away"-13.7525.76null2.761.08"DFL-OBJ-J01APO|away""DFL-OBJ-0026PM""away"0.7733.139999null13.550.0"DFL-OBJ-0026PM|away""DFL-OBJ-002G4A""home"10.16-3.39null10.91.13"DFL-OBJ-002G4A|home""DFL-CLU-00000G""#25282A""FC Bayern München""DFL-CLU-000008""#F4F5F0""1. FC Köln"
nullnull"DFL-MAT-J03WMX"10003"first_half"2023-05-27 13:30:12.720"referee:DFL-OBJ-00011J"null-20.4834.200001null0.471.09"referee:DFL-OBJ-00011J|none""DFL-OBJ-002GKO""home"11.295.33null5.121.13"DFL-OBJ-002GKO|home""DFL-OBJ-0002DR""away"-46.6500020.18null2.490.96"DFL-OBJ-0002DR|away"nullnullnullnullnullnullnullnull19.889999-34.439999null2.191.35"referee:DFL-OBJ-00011W|none""DFL-OBJ-J01APO""away"-13.7325.780001null2.890.91"DFL-OBJ-J01APO|away""DFL-OBJ-0026PM""away"0.9333.099998null14.460.0"DFL-OBJ-0026PM|away""DFL-OBJ-002G4A""home"10.03-3.39null11.061.03"DFL-OBJ-002G4A|home""DFL-CLU-00000G""#25282A""FC Bayern München""DFL-CLU-000008""#F4F5F0""1. FC Köln"
nullnull"DFL-MAT-J03WMX"10004"first_half"2023-05-27 13:30:12.760"referee:DFL-OBJ-00011J"null-20.4834.209999null0.631.17"referee:DFL-OBJ-00011J|none""DFL-OBJ-002GKO""home"11.265.38null5.280.98"DFL-OBJ-002GKO|home""DFL-OBJ-0002DR""away"-46.6199990.18null2.630.95"DFL-OBJ-0002DR|away"nullnullnullnullnullnullnullnull19.860001-34.439999null2.381.25"referee:DFL-OBJ-00011W|none""DFL-OBJ-J01APO""away"-13.7125.809999null3.00.72"DFL-OBJ-J01APO|away""DFL-OBJ-0026PM""away"1.133.060001null15.420.0"DFL-OBJ-0026PM|away""DFL-OBJ-002G4A""home"9.91-3.39null11.190.92"DFL-OBJ-002G4A|home""DFL-CLU-00000G""#25282A""FC Bayern München""DFL-CLU-000008""#F4F5F0""1. FC Köln"
" ], "text/plain": [ "shape: (5, 316)\n", "┌───────────┬───────────┬───────────┬──────────┬───┬───────────┬───────────┬───────────┬───────────┐\n", - "│ competiti ┆ season/id ┆ match/id ┆ frame_id ┆ … ┆ teams/tea ┆ teams/tea ┆ teams/tea ┆ teams/tea │\n", - "│ on/id ┆ --- ┆ --- ┆ --- ┆ ┆ m:home/je ┆ m:away/id ┆ m:away/na ┆ m:away/je │\n", - "│ --- ┆ str ┆ str ┆ i64 ┆ ┆ rsey_colo ┆ --- ┆ me ┆ rsey_colo │\n", - "│ str ┆ ┆ ┆ ┆ ┆ ur ┆ str ┆ --- ┆ ur │\n", - "│ ┆ ┆ ┆ ┆ ┆ --- ┆ ┆ str ┆ --- │\n", - "│ ┆ ┆ ┆ ┆ ┆ str ┆ ┆ ┆ str │\n", + "│ competiti ┆ season/id ┆ match/id ┆ frame_id ┆ … ┆ teams/awa ┆ teams/hom ┆ teams/hom ┆ teams/hom │\n", + "│ on/id ┆ --- ┆ --- ┆ --- ┆ ┆ y/name ┆ e/id ┆ e/jersey_ ┆ e/name │\n", + "│ --- ┆ str ┆ str ┆ i64 ┆ ┆ --- ┆ --- ┆ colour ┆ --- │\n", + "│ str ┆ ┆ ┆ ┆ ┆ str ┆ str ┆ --- ┆ str │\n", + "│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ str ┆ │\n", "╞═══════════╪═══════════╪═══════════╪══════════╪═══╪═══════════╪═══════════╪═══════════╪═══════════╡\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10000 ┆ … ┆ #F4F5F0 ┆ DFL-CLU-0 ┆ FC Bayern ┆ #25282A │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ ┆ 0000G ┆ München ┆ │\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10001 ┆ … ┆ #F4F5F0 ┆ DFL-CLU-0 ┆ FC Bayern ┆ #25282A │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ ┆ 0000G ┆ München ┆ │\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10002 ┆ … ┆ #F4F5F0 ┆ DFL-CLU-0 ┆ FC Bayern ┆ #25282A │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ ┆ 0000G ┆ München ┆ │\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10003 ┆ … ┆ #F4F5F0 ┆ DFL-CLU-0 ┆ FC Bayern ┆ #25282A │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ ┆ 0000G ┆ München ┆ │\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10004 ┆ … ┆ #F4F5F0 ┆ DFL-CLU-0 ┆ FC Bayern ┆ #25282A │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ ┆ 0000G ┆ München ┆ │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10000 ┆ … ┆ FC Bayern ┆ DFL-CLU-0 ┆ #F4F5F0 ┆ 1. FC │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ München ┆ 00008 ┆ ┆ Köln │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10001 ┆ … ┆ FC Bayern ┆ DFL-CLU-0 ┆ #F4F5F0 ┆ 1. FC │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ München ┆ 00008 ┆ ┆ Köln │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10002 ┆ … ┆ FC Bayern ┆ DFL-CLU-0 ┆ #F4F5F0 ┆ 1. FC │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ München ┆ 00008 ┆ ┆ Köln │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10003 ┆ … ┆ FC Bayern ┆ DFL-CLU-0 ┆ #F4F5F0 ┆ 1. FC │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ München ┆ 00008 ┆ ┆ Köln │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10004 ┆ … ┆ FC Bayern ┆ DFL-CLU-0 ┆ #F4F5F0 ┆ 1. FC │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ München ┆ 00008 ┆ ┆ Köln │\n", "└───────────┴───────────┴───────────┴──────────┴───┴───────────┴───────────┴───────────┴───────────┘" ] }, @@ -308,48 +307,44 @@ "data": { "text/plain": [ "{'_entities': {'ball': {},\n", - " 'teams': {'away': {'players': {'DFL-OBJ-0000IA': {},\n", - " 'DFL-OBJ-J01APO': {},\n", - " 'DFL-OBJ-002GCR': {},\n", - " 'DFL-OBJ-0002DR': {},\n", - " 'DFL-OBJ-J01N65': {},\n", - " 'DFL-OBJ-0002F5': {},\n", + " 'teams': {'away': {'players': {'DFL-OBJ-0002DR': {},\n", " 'DFL-OBJ-J01B8N': {},\n", + " 'DFL-OBJ-J01N65': {},\n", + " 'DFL-OBJ-J01APO': {},\n", " 'DFL-OBJ-0000RP': {},\n", - " 'DFL-OBJ-0002AU': {},\n", - " 'DFL-OBJ-0027G6': {},\n", " 'DFL-OBJ-0027KL': {},\n", - " 'DFL-OBJ-J015S4': {},\n", - " 'DFL-OBJ-000191': {},\n", + " 'DFL-OBJ-0027G6': {},\n", " 'DFL-OBJ-0026PM': {},\n", " 'DFL-OBJ-0027G0': {},\n", - " 'DFL-OBJ-J017RE': {}},\n", - " 'attributes': {}},\n", - " 'home': {'players': {'DFL-OBJ-002GBW': {},\n", + " 'DFL-OBJ-J015S4': {},\n", + " 'DFL-OBJ-0002F5': {},\n", + " 'DFL-OBJ-002GCR': {},\n", + " 'DFL-OBJ-J017RE': {},\n", + " 'DFL-OBJ-000191': {},\n", + " 'DFL-OBJ-0002AU': {},\n", + " 'DFL-OBJ-0000IA': {}},\n", + " 'id': 'DFL-CLU-00000G',\n", + " 'jersey_colour': '#25282A',\n", + " 'name': 'FC Bayern München'},\n", + " 'home': {'players': {'DFL-OBJ-0027V2': {},\n", + " 'DFL-OBJ-002GKO': {},\n", + " 'DFL-OBJ-002GBW': {},\n", + " 'DFL-OBJ-000270': {},\n", " 'DFL-OBJ-J01BGM': {},\n", - " 'DFL-OBJ-002G4A': {},\n", - " 'DFL-OBJ-J01DVC': {},\n", + " 'DFL-OBJ-002G89': {},\n", + " 'DFL-OBJ-J01D1W': {},\n", " 'DFL-OBJ-0002HE': {},\n", - " 'DFL-OBJ-000270': {},\n", " 'DFL-OBJ-0027AX': {},\n", + " 'DFL-OBJ-J01DVC': {},\n", + " 'DFL-OBJ-00012X': {},\n", " 'DFL-OBJ-0002BO': {},\n", - " 'DFL-OBJ-0027V2': {},\n", - " 'DFL-OBJ-J01D1W': {},\n", - " 'DFL-OBJ-0026ZI': {},\n", " 'DFL-OBJ-0028BD': {},\n", - " 'DFL-OBJ-002GKO': {},\n", - " 'DFL-OBJ-00012X': {},\n", " 'DFL-OBJ-0027B6': {},\n", - " 'DFL-OBJ-002G89': {}},\n", - " 'attributes': {}},\n", - " 'team:home': {'players': {},\n", - " 'attributes': {'id': 'DFL-CLU-000008',\n", - " 'name': '1. FC Köln',\n", - " 'jersey_colour': '#F4F5F0'}},\n", - " 'team:away': {'players': {},\n", - " 'attributes': {'id': 'DFL-CLU-00000G',\n", - " 'name': 'FC Bayern München',\n", - " 'jersey_colour': '#25282A'}}},\n", + " 'DFL-OBJ-0026ZI': {},\n", + " 'DFL-OBJ-002G4A': {}},\n", + " 'id': 'DFL-CLU-000008',\n", + " 'jersey_colour': '#F4F5F0',\n", + " 'name': '1. FC Köln'}},\n", " 'referees': {'referee:DFL-OBJ-000141': {},\n", " 'referee:DFL-OBJ-00011J': {},\n", " 'referee:DFL-OBJ-00011W': {}}}}" diff --git a/python/tracking_data_container/catalog.py b/python/tracking_data_container/catalog.py index efce0ba..6736369 100644 --- a/python/tracking_data_container/catalog.py +++ b/python/tracking_data_container/catalog.py @@ -575,16 +575,24 @@ def schema(self) -> Fields: for row in entities: p_id = str(row.get("entity_id")) t_id = str(row.get("team_id", "none")) - prefix = TrackedEntity.from_entity_key(f"{p_id}|{t_id}").cdf_prefix() + entity = TrackedEntity.from_entity_key(f"{p_id}|{t_id}") + prefix = entity.cdf_prefix() for attr in attr_cols: - wide_names.append(f"{prefix}{attr}") + if attr != "entity_key": + wide_names.append(f"{prefix}{attr}") for attr in row.keys(): - if attr not in ("entity_id", "team_id"): + if attr not in ("entity_id", "team_id", "entity_key"): wide_names.append(f"{prefix}{attr}") - wide_names.append(f"{prefix}player/id") - wide_names.append(f"{prefix}team/id") + # Always add player/id (except for ball which has it via TrackedEntity logic if needed, + # but usually ball doesn't have player/id) + if not entity.is_ball: + wide_names.append(f"{prefix}player/id") + + # Only add team/id for players and teams + if entity.is_player or entity.is_team: + wide_names.append(f"{prefix}team/id") self._cached_schema = Fields(list(dict.fromkeys(wide_names))) return self._cached_schema @@ -730,7 +738,7 @@ def collect(self) -> TrackingDataContainer: t_id = ent_data["team_id"] prefix = TrackedEntity.from_entity_key(f"{p_id}|{t_id}").cdf_prefix() for attr, value in ent_data.items(): - if attr in ("entity_id", "team_id"): + if attr in ("entity_id", "team_id", "entity_key"): continue wide_attr_name = f"{prefix}{attr}" From 8a08119073373c695224246aee77f59aedbda98f Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Wed, 8 Apr 2026 16:38:36 +0200 Subject: [PATCH 18/21] feat: use IcebergConfig for default catalog configuration and optimization --- examples/08-idsse-warehouse.ipynb | 102 +++++++++--------- examples/idsse_warehouse/catalog.db | Bin 0 -> 20480 bytes ...-96a2-48d6-8305-1e7155f857e6.metadata.json | 1 + ...-4270-4bd2-86dc-2b88858c0833.metadata.json | 1 + ...-c362-4a0f-a942-2fc8d6c51c96.metadata.json | 1 + ...-b298-4f4f-a814-9b9f76d8e71a.metadata.json | 1 + ...c097fb-ed6a-4e53-95aa-61c36e19e9b5-m0.avro | Bin 0 -> 4505 bytes ...8d49d5-48bd-40a1-aac5-ac3df2728952-m0.avro | Bin 0 -> 4508 bytes ...b6cbfa-fef9-4779-b2e1-c63290db081c-m0.avro | Bin 0 -> 4507 bytes ...-eab6cbfa-fef9-4779-b2e1-c63290db081c.avro | Bin 0 -> 1947 bytes ...-13c097fb-ed6a-4e53-95aa-61c36e19e9b5.avro | Bin 0 -> 1902 bytes ...-748d49d5-48bd-40a1-aac5-ac3df2728952.avro | Bin 0 -> 1827 bytes ...-0ccc-4e61-aa83-a184ad41f6f1.metadata.json | 1 + ...-f126-4491-bfa6-00cf45343e37.metadata.json | 1 + ...-ab81-46c9-92b2-74595885722c.metadata.json | 1 + ...-78aa-49d0-8fa1-f6314d7b0ee9.metadata.json | 1 + ...6c935e-8816-455f-987e-f46e735afb70-m0.avro | Bin 0 -> 5590 bytes ...a2fe75-8006-42c8-97d9-5e4835aa023e-m0.avro | Bin 0 -> 6126 bytes ...eff7a4-107d-40c3-846c-6d69ba0f72ff-m0.avro | Bin 0 -> 6110 bytes ...-3aa2fe75-8006-42c8-97d9-5e4835aa023e.avro | Bin 0 -> 1851 bytes ...-236c935e-8816-455f-987e-f46e735afb70.avro | Bin 0 -> 2001 bytes ...-b2eff7a4-107d-40c3-846c-6d69ba0f72ff.avro | Bin 0 -> 1936 bytes python/tests/test_catalog.py | 3 +- python/tracking_data_container/catalog.py | 35 +++++- python/tracking_data_container/config.py | 5 +- 25 files changed, 93 insertions(+), 60 deletions(-) create mode 100644 examples/idsse_warehouse/catalog.db create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00000-f1bd0a1f-96a2-48d6-8305-1e7155f857e6.metadata.json create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00001-ee062ad7-4270-4bd2-86dc-2b88858c0833.metadata.json create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00002-9c72b796-c362-4a0f-a942-2fc8d6c51c96.metadata.json create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00003-b3d28f12-b298-4f4f-a814-9b9f76d8e71a.metadata.json create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/13c097fb-ed6a-4e53-95aa-61c36e19e9b5-m0.avro create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/748d49d5-48bd-40a1-aac5-ac3df2728952-m0.avro create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/eab6cbfa-fef9-4779-b2e1-c63290db081c-m0.avro create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/snap-2148665455330551612-0-eab6cbfa-fef9-4779-b2e1-c63290db081c.avro create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/snap-2774007322244479359-0-13c097fb-ed6a-4e53-95aa-61c36e19e9b5.avro create mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/snap-5536800293271387766-0-748d49d5-48bd-40a1-aac5-ac3df2728952.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00000-eb9e2be6-0ccc-4e61-aa83-a184ad41f6f1.metadata.json create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00001-bf153c68-f126-4491-bfa6-00cf45343e37.metadata.json create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00002-d345f9cd-ab81-46c9-92b2-74595885722c.metadata.json create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00003-97d8ffab-78aa-49d0-8fa1-f6314d7b0ee9.metadata.json create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/236c935e-8816-455f-987e-f46e735afb70-m0.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/3aa2fe75-8006-42c8-97d9-5e4835aa023e-m0.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/b2eff7a4-107d-40c3-846c-6d69ba0f72ff-m0.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-3805096703135495509-0-3aa2fe75-8006-42c8-97d9-5e4835aa023e.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-6127150891889527239-0-236c935e-8816-455f-987e-f46e735afb70.avro create mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-6133585602095476169-0-b2eff7a4-107d-40c3-846c-6d69ba0f72ff.avro diff --git a/examples/08-idsse-warehouse.ipynb b/examples/08-idsse-warehouse.ipynb index 710a801..23f1014 100644 --- a/examples/08-idsse-warehouse.ipynb +++ b/examples/08-idsse-warehouse.ipynb @@ -138,15 +138,15 @@ "text": [ "\n", "Processing match DFL-MAT-J03WMX...\n", - " ✅ Parsed in 1.94s, Saved in 5.75s\n", + " ✅ Parsed in 14.33s, Saved in 12.99s\n", " ✅ Rows: 145,967\n", "\n", "Processing match DFL-MAT-J03WN1...\n", - " ✅ Parsed in 1.83s, Saved in 5.09s\n", + " ✅ Parsed in 14.03s, Saved in 12.06s\n", " ✅ Rows: 141,561\n", "\n", "Processing match DFL-MAT-J03WOH...\n", - " ✅ Parsed in 1.46s, Saved in 4.33s\n", + " ✅ Parsed in 12.71s, Saved in 10.63s\n", " ✅ Rows: 137,214\n" ] } @@ -242,7 +242,7 @@ "output_type": "stream", "text": [ "Loading data for DFL-MAT-J03WMX...\n", - " ✅ Loaded and pivoted 165259 rows in 1.5354s\n" + " ✅ Loaded and pivoted 165259 rows in 7.1870s\n" ] }, { @@ -255,27 +255,27 @@ " white-space: pre-wrap;\n", "}\n", "\n", - "shape: (5, 316)
competition/idseason/idmatch/idframe_idperiodtimestampreferees/DFL-OBJ-00011J/player/idreferees/DFL-OBJ-00011J/team/idreferees/DFL-OBJ-00011J/xreferees/DFL-OBJ-00011J/yreferees/DFL-OBJ-00011J/zreferees/DFL-OBJ-00011J/velreferees/DFL-OBJ-00011J/accreferees/DFL-OBJ-00011J/entity_keyteams/home/players/DFL-OBJ-002GKO/player/idteams/home/players/DFL-OBJ-002GKO/team/idteams/home/players/DFL-OBJ-002GKO/xteams/home/players/DFL-OBJ-002GKO/yteams/home/players/DFL-OBJ-002GKO/zteams/home/players/DFL-OBJ-002GKO/velteams/home/players/DFL-OBJ-002GKO/accteams/home/players/DFL-OBJ-002GKO/entity_keyteams/away/players/DFL-OBJ-0002DR/player/idteams/away/players/DFL-OBJ-0002DR/team/idteams/away/players/DFL-OBJ-0002DR/xteams/away/players/DFL-OBJ-0002DR/yteams/away/players/DFL-OBJ-0002DR/zteams/away/players/DFL-OBJ-0002DR/velteams/away/players/DFL-OBJ-0002DR/accteams/away/players/DFL-OBJ-0002DR/entity_keyteams/home/players/DFL-OBJ-002GBW/player/idteams/home/players/DFL-OBJ-002GBW/team/idteams/home/players/DFL-OBJ-002GBW/xteams/home/players/DFL-OBJ-002GBW/yteams/home/players/DFL-OBJ-002GBW/zteams/home/players/DFL-OBJ-002GBW/velteams/home/players/DFL-OBJ-002GBW/accreferees/DFL-OBJ-00011W/team/idreferees/DFL-OBJ-00011W/xreferees/DFL-OBJ-00011W/yreferees/DFL-OBJ-00011W/zreferees/DFL-OBJ-00011W/velreferees/DFL-OBJ-00011W/accreferees/DFL-OBJ-00011W/entity_keyteams/away/players/DFL-OBJ-J01APO/player/idteams/away/players/DFL-OBJ-J01APO/team/idteams/away/players/DFL-OBJ-J01APO/xteams/away/players/DFL-OBJ-J01APO/yteams/away/players/DFL-OBJ-J01APO/zteams/away/players/DFL-OBJ-J01APO/velteams/away/players/DFL-OBJ-J01APO/accteams/away/players/DFL-OBJ-J01APO/entity_keyteams/away/players/DFL-OBJ-0026PM/player/idteams/away/players/DFL-OBJ-0026PM/team/idteams/away/players/DFL-OBJ-0026PM/xteams/away/players/DFL-OBJ-0026PM/yteams/away/players/DFL-OBJ-0026PM/zteams/away/players/DFL-OBJ-0026PM/velteams/away/players/DFL-OBJ-0026PM/accteams/away/players/DFL-OBJ-0026PM/entity_keyteams/home/players/DFL-OBJ-002G4A/player/idteams/home/players/DFL-OBJ-002G4A/team/idteams/home/players/DFL-OBJ-002G4A/xteams/home/players/DFL-OBJ-002G4A/yteams/home/players/DFL-OBJ-002G4A/zteams/home/players/DFL-OBJ-002G4A/velteams/home/players/DFL-OBJ-002G4A/accteams/home/players/DFL-OBJ-002G4A/entity_keyteams/away/idteams/away/jersey_colourteams/away/nameteams/home/idteams/home/jersey_colourteams/home/name
strstrstri64strdatetime[μs]strstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64strf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrf32f32f32f64f64strstrstrstrstrstrstr
nullnull"DFL-MAT-J03WMX"10000"first_half"2023-05-27 13:30:12.600"referee:DFL-OBJ-00011J"null-20.4934.200001null0.00.0"referee:DFL-OBJ-00011J|none""DFL-OBJ-002GKO""home"11.375.18null0.00.0"DFL-OBJ-002GKO|home""DFL-OBJ-0002DR""away"-46.730.18null0.00.0"DFL-OBJ-0002DR|away"nullnullnullnullnullnullnullnull19.950001-34.450001null0.00.0"referee:DFL-OBJ-00011W|none""DFL-OBJ-J01APO""away"-13.825.719999null0.00.0"DFL-OBJ-J01APO|away""DFL-OBJ-0026PM""away"0.4233.220001null0.00.0"DFL-OBJ-0026PM|away""DFL-OBJ-002G4A""home"10.39-3.39null0.00.0"DFL-OBJ-002G4A|home""DFL-CLU-00000G""#25282A""FC Bayern München""DFL-CLU-000008""#F4F5F0""1. FC Köln"
nullnull"DFL-MAT-J03WMX"10001"first_half"2023-05-27 13:30:12.640"referee:DFL-OBJ-00011J"null-20.4934.200001null0.190.86"referee:DFL-OBJ-00011J|none""DFL-OBJ-002GKO""home"11.345.23null4.771.35"DFL-OBJ-002GKO|home""DFL-OBJ-0002DR""away"-46.7099990.18null2.211.01"DFL-OBJ-0002DR|away"nullnullnullnullnullnullnullnull19.92-34.450001null1.771.48"referee:DFL-OBJ-00011W|none""DFL-OBJ-J01APO""away"-13.7825.74null2.591.23"DFL-OBJ-J01APO|away""DFL-OBJ-0026PM""away"0.5933.18null12.60.0"DFL-OBJ-0026PM|away""DFL-OBJ-002G4A""home"10.28-3.39null10.721.22"DFL-OBJ-002G4A|home""DFL-CLU-00000G""#25282A""FC Bayern München""DFL-CLU-000008""#F4F5F0""1. FC Köln"
nullnull"DFL-MAT-J03WMX"10002"first_half"2023-05-27 13:30:12.680"referee:DFL-OBJ-00011J"null-20.4934.200001null0.320.99"referee:DFL-OBJ-00011J|none""DFL-OBJ-002GKO""home"11.315.28null4.951.26"DFL-OBJ-002GKO|home""DFL-OBJ-0002DR""away"-46.680.18null2.350.98"DFL-OBJ-0002DR|away"nullnullnullnullnullnullnullnull19.91-34.450001null1.981.42"referee:DFL-OBJ-00011W|none""DFL-OBJ-J01APO""away"-13.7525.76null2.761.08"DFL-OBJ-J01APO|away""DFL-OBJ-0026PM""away"0.7733.139999null13.550.0"DFL-OBJ-0026PM|away""DFL-OBJ-002G4A""home"10.16-3.39null10.91.13"DFL-OBJ-002G4A|home""DFL-CLU-00000G""#25282A""FC Bayern München""DFL-CLU-000008""#F4F5F0""1. FC Köln"
nullnull"DFL-MAT-J03WMX"10003"first_half"2023-05-27 13:30:12.720"referee:DFL-OBJ-00011J"null-20.4834.200001null0.471.09"referee:DFL-OBJ-00011J|none""DFL-OBJ-002GKO""home"11.295.33null5.121.13"DFL-OBJ-002GKO|home""DFL-OBJ-0002DR""away"-46.6500020.18null2.490.96"DFL-OBJ-0002DR|away"nullnullnullnullnullnullnullnull19.889999-34.439999null2.191.35"referee:DFL-OBJ-00011W|none""DFL-OBJ-J01APO""away"-13.7325.780001null2.890.91"DFL-OBJ-J01APO|away""DFL-OBJ-0026PM""away"0.9333.099998null14.460.0"DFL-OBJ-0026PM|away""DFL-OBJ-002G4A""home"10.03-3.39null11.061.03"DFL-OBJ-002G4A|home""DFL-CLU-00000G""#25282A""FC Bayern München""DFL-CLU-000008""#F4F5F0""1. FC Köln"
nullnull"DFL-MAT-J03WMX"10004"first_half"2023-05-27 13:30:12.760"referee:DFL-OBJ-00011J"null-20.4834.209999null0.631.17"referee:DFL-OBJ-00011J|none""DFL-OBJ-002GKO""home"11.265.38null5.280.98"DFL-OBJ-002GKO|home""DFL-OBJ-0002DR""away"-46.6199990.18null2.630.95"DFL-OBJ-0002DR|away"nullnullnullnullnullnullnullnull19.860001-34.439999null2.381.25"referee:DFL-OBJ-00011W|none""DFL-OBJ-J01APO""away"-13.7125.809999null3.00.72"DFL-OBJ-J01APO|away""DFL-OBJ-0026PM""away"1.133.060001null15.420.0"DFL-OBJ-0026PM|away""DFL-OBJ-002G4A""home"9.91-3.39null11.190.92"DFL-OBJ-002G4A|home""DFL-CLU-00000G""#25282A""FC Bayern München""DFL-CLU-000008""#F4F5F0""1. FC Köln"
" + "shape: (5, 273)
competition/idseason/idmatch/idframe_idperiodtimestampteams/home/players/DFL-OBJ-0026ZI/player/idteams/home/players/DFL-OBJ-0026ZI/team/idteams/home/players/DFL-OBJ-0026ZI/xteams/home/players/DFL-OBJ-0026ZI/yteams/home/players/DFL-OBJ-0026ZI/zteams/home/players/DFL-OBJ-0026ZI/velteams/home/players/DFL-OBJ-0026ZI/accteams/home/players/DFL-OBJ-0002BO/player/idteams/home/players/DFL-OBJ-0002BO/team/idteams/home/players/DFL-OBJ-0002BO/xteams/home/players/DFL-OBJ-0002BO/yteams/home/players/DFL-OBJ-0002BO/zteams/home/players/DFL-OBJ-0002BO/velteams/home/players/DFL-OBJ-0002BO/accteams/home/players/DFL-OBJ-0027B6/player/idteams/home/players/DFL-OBJ-0027B6/team/idteams/home/players/DFL-OBJ-0027B6/xteams/home/players/DFL-OBJ-0027B6/yteams/home/players/DFL-OBJ-0027B6/zteams/home/players/DFL-OBJ-0027B6/velteams/home/players/DFL-OBJ-0027B6/accteams/away/players/DFL-OBJ-002GCR/player/idteams/away/players/DFL-OBJ-002GCR/team/idteams/away/players/DFL-OBJ-002GCR/xteams/away/players/DFL-OBJ-002GCR/yteams/away/players/DFL-OBJ-002GCR/zteams/away/players/DFL-OBJ-002GCR/velteams/away/players/DFL-OBJ-002GCR/accteams/away/players/DFL-OBJ-0027G0/player/idteams/away/players/DFL-OBJ-0027G0/team/idteams/away/players/DFL-OBJ-0027G0/xteams/home/players/DFL-OBJ-002GBW/zteams/home/players/DFL-OBJ-002GBW/velteams/home/players/DFL-OBJ-002GBW/accteams/away/player/idteams/away/team/idteams/away/xteams/away/yteams/away/zteams/away/velteams/away/accteams/home/players/DFL-OBJ-0028BD/player/idteams/home/players/DFL-OBJ-0028BD/team/idteams/home/players/DFL-OBJ-0028BD/xteams/home/players/DFL-OBJ-0028BD/yteams/home/players/DFL-OBJ-0028BD/zteams/home/players/DFL-OBJ-0028BD/velteams/home/players/DFL-OBJ-0028BD/accteams/away/players/DFL-OBJ-0002F5/player/idteams/away/players/DFL-OBJ-0002F5/team/idteams/away/players/DFL-OBJ-0002F5/xteams/away/players/DFL-OBJ-0002F5/yteams/away/players/DFL-OBJ-0002F5/zteams/away/players/DFL-OBJ-0002F5/velteams/away/players/DFL-OBJ-0002F5/accteams/away/players/DFL-OBJ-J015S4/player/idteams/away/players/DFL-OBJ-J015S4/team/idteams/away/players/DFL-OBJ-J015S4/xteams/away/players/DFL-OBJ-J015S4/yteams/away/players/DFL-OBJ-J015S4/zteams/away/players/DFL-OBJ-J015S4/velteams/away/players/DFL-OBJ-J015S4/accteams/away/jersey_colourteams/away/nameteams/away/idteams/home/jersey_colourteams/home/nameteams/home/id
strstrstri64strdatetime[μs]strstrf32f32f32f64f64strstrf32f32f32f64f64strstrf32f32f32f64f64strstrf32f32f32f64f64strstrf32f32f64f64strstrf32f32f32f64f64strstrf32f32f32f64f64strstrf32f32f32f64f64strstrf32f32f32f64f64strstrstrstrstrstr
nullnull"DFL-MAT-J03WMX"10000"first_half"2023-05-27 13:30:12.600nullnullnullnullnullnullnull"DFL-OBJ-0002BO""home"17.2620.85null0.00.0"DFL-OBJ-0027B6""home"20.167.08null0.00.0nullnullnullnullnullnullnull"DFL-OBJ-0027G0""away"-12.8nullnullnull"team:away""away"nullnullnullnullnull"DFL-OBJ-0028BD""home"0.44-15.57null0.00.0"DFL-OBJ-0002F5""away"-9.09-1.96null0.00.0nullnullnullnullnullnullnull"#25282A""FC Bayern München""DFL-CLU-00000G""#F4F5F0""1. FC Köln""DFL-CLU-000008"
nullnull"DFL-MAT-J03WMX"10001"first_half"2023-05-27 13:30:12.640nullnullnullnullnullnullnull"DFL-OBJ-0002BO""home"17.2320.879999null4.431.08"DFL-OBJ-0027B6""home"20.1200017.09null2.980.74nullnullnullnullnullnullnull"DFL-OBJ-0027G0""away"-12.83nullnullnull"team:away""away"nullnullnullnullnull"DFL-OBJ-0028BD""home"0.37-15.57null6.912.7"DFL-OBJ-0002F5""away"-9.06-1.95null3.10.0nullnullnullnullnullnullnull"#25282A""FC Bayern München""DFL-CLU-00000G""#F4F5F0""1. FC Köln""DFL-CLU-000008"
nullnull"DFL-MAT-J03WMX"10002"first_half"2023-05-27 13:30:12.680nullnullnullnullnullnullnull"DFL-OBJ-0002BO""home"17.1820.91null4.61.07"DFL-OBJ-0027B6""home"20.097.1null3.090.65nullnullnullnullnullnullnull"DFL-OBJ-0027G0""away"-12.88nullnullnull"team:away""away"nullnullnullnullnull"DFL-OBJ-0028BD""home"0.29-15.57null7.312.69"DFL-OBJ-0002F5""away"-9.02-1.94null3.170.0nullnullnullnullnullnullnull"#25282A""FC Bayern München""DFL-CLU-00000G""#F4F5F0""1. FC Köln""DFL-CLU-000008"
nullnull"DFL-MAT-J03WMX"10003"first_half"2023-05-27 13:30:12.720nullnullnullnullnullnullnull"DFL-OBJ-0002BO""home"17.1520.940001null4.741.03"DFL-OBJ-0027B6""home"20.0599997.12null3.180.56nullnullnullnullnullnullnull"DFL-OBJ-0027G0""away"-12.91nullnullnull"team:away""away"nullnullnullnullnull"DFL-OBJ-0028BD""home"0.21-15.57null7.692.66"DFL-OBJ-0002F5""away"-8.99-1.93null3.290.0nullnullnullnullnullnullnull"#25282A""FC Bayern München""DFL-CLU-00000G""#F4F5F0""1. FC Köln""DFL-CLU-000008"
nullnull"DFL-MAT-J03WMX"10004"first_half"2023-05-27 13:30:12.760nullnullnullnullnullnullnull"DFL-OBJ-0002BO""home"17.11000120.98null4.880.98"DFL-OBJ-0027B6""home"20.027.13null3.250.44nullnullnullnullnullnullnull"DFL-OBJ-0027G0""away"-12.94nullnullnull"team:away""away"nullnullnullnullnull"DFL-OBJ-0028BD""home"0.12-15.57null8.072.62"DFL-OBJ-0002F5""away"-8.96-1.91null3.440.0nullnullnullnullnullnullnull"#25282A""FC Bayern München""DFL-CLU-00000G""#F4F5F0""1. FC Köln""DFL-CLU-000008"
" ], "text/plain": [ - "shape: (5, 316)\n", + "shape: (5, 273)\n", "┌───────────┬───────────┬───────────┬──────────┬───┬───────────┬───────────┬───────────┬───────────┐\n", "│ competiti ┆ season/id ┆ match/id ┆ frame_id ┆ … ┆ teams/awa ┆ teams/hom ┆ teams/hom ┆ teams/hom │\n", - "│ on/id ┆ --- ┆ --- ┆ --- ┆ ┆ y/name ┆ e/id ┆ e/jersey_ ┆ e/name │\n", - "│ --- ┆ str ┆ str ┆ i64 ┆ ┆ --- ┆ --- ┆ colour ┆ --- │\n", - "│ str ┆ ┆ ┆ ┆ ┆ str ┆ str ┆ --- ┆ str │\n", - "│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ str ┆ │\n", + "│ on/id ┆ --- ┆ --- ┆ --- ┆ ┆ y/id ┆ e/jersey_ ┆ e/name ┆ e/id │\n", + "│ --- ┆ str ┆ str ┆ i64 ┆ ┆ --- ┆ colour ┆ --- ┆ --- │\n", + "│ str ┆ ┆ ┆ ┆ ┆ str ┆ --- ┆ str ┆ str │\n", + "│ ┆ ┆ ┆ ┆ ┆ ┆ str ┆ ┆ │\n", "╞═══════════╪═══════════╪═══════════╪══════════╪═══╪═══════════╪═══════════╪═══════════╪═══════════╡\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10000 ┆ … ┆ FC Bayern ┆ DFL-CLU-0 ┆ #F4F5F0 ┆ 1. FC │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ München ┆ 00008 ┆ ┆ Köln │\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10001 ┆ … ┆ FC Bayern ┆ DFL-CLU-0 ┆ #F4F5F0 ┆ 1. FC │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ München ┆ 00008 ┆ ┆ Köln │\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10002 ┆ … ┆ FC Bayern ┆ DFL-CLU-0 ┆ #F4F5F0 ┆ 1. FC │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ München ┆ 00008 ┆ ┆ Köln │\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10003 ┆ … ┆ FC Bayern ┆ DFL-CLU-0 ┆ #F4F5F0 ┆ 1. FC │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ München ┆ 00008 ┆ ┆ Köln │\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10004 ┆ … ┆ FC Bayern ┆ DFL-CLU-0 ┆ #F4F5F0 ┆ 1. FC │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ München ┆ 00008 ┆ ┆ Köln │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10000 ┆ … ┆ DFL-CLU-0 ┆ #F4F5F0 ┆ 1. FC ┆ DFL-CLU-0 │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ 0000G ┆ ┆ Köln ┆ 00008 │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10001 ┆ … ┆ DFL-CLU-0 ┆ #F4F5F0 ┆ 1. FC ┆ DFL-CLU-0 │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ 0000G ┆ ┆ Köln ┆ 00008 │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10002 ┆ … ┆ DFL-CLU-0 ┆ #F4F5F0 ┆ 1. FC ┆ DFL-CLU-0 │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ 0000G ┆ ┆ Köln ┆ 00008 │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10003 ┆ … ┆ DFL-CLU-0 ┆ #F4F5F0 ┆ 1. FC ┆ DFL-CLU-0 │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ 0000G ┆ ┆ Köln ┆ 00008 │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10004 ┆ … ┆ DFL-CLU-0 ┆ #F4F5F0 ┆ 1. FC ┆ DFL-CLU-0 │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ 0000G ┆ ┆ Köln ┆ 00008 │\n", "└───────────┴───────────┴───────────┴──────────┴───┴───────────┴───────────┴───────────┴───────────┘" ] }, @@ -307,47 +307,47 @@ "data": { "text/plain": [ "{'_entities': {'ball': {},\n", - " 'teams': {'away': {'players': {'DFL-OBJ-0002DR': {},\n", - " 'DFL-OBJ-J01B8N': {},\n", - " 'DFL-OBJ-J01N65': {},\n", - " 'DFL-OBJ-J01APO': {},\n", - " 'DFL-OBJ-0000RP': {},\n", - " 'DFL-OBJ-0027KL': {},\n", - " 'DFL-OBJ-0027G6': {},\n", - " 'DFL-OBJ-0026PM': {},\n", - " 'DFL-OBJ-0027G0': {},\n", - " 'DFL-OBJ-J015S4': {},\n", - " 'DFL-OBJ-0002F5': {},\n", - " 'DFL-OBJ-002GCR': {},\n", + " 'teams': {'away': {'players': {'DFL-OBJ-J015S4': {},\n", " 'DFL-OBJ-J017RE': {},\n", + " 'DFL-OBJ-0000IA': {},\n", + " 'DFL-OBJ-0002DR': {},\n", " 'DFL-OBJ-000191': {},\n", + " 'DFL-OBJ-0027G6': {},\n", + " 'DFL-OBJ-0000RP': {},\n", + " 'DFL-OBJ-J01B8N': {},\n", + " 'DFL-OBJ-0027G0': {},\n", " 'DFL-OBJ-0002AU': {},\n", - " 'DFL-OBJ-0000IA': {}},\n", - " 'id': 'DFL-CLU-00000G',\n", + " 'DFL-OBJ-002GCR': {},\n", + " 'DFL-OBJ-0026PM': {},\n", + " 'DFL-OBJ-0002F5': {},\n", + " 'DFL-OBJ-J01APO': {},\n", + " 'DFL-OBJ-J01N65': {},\n", + " 'DFL-OBJ-0027KL': {}},\n", " 'jersey_colour': '#25282A',\n", - " 'name': 'FC Bayern München'},\n", - " 'home': {'players': {'DFL-OBJ-0027V2': {},\n", - " 'DFL-OBJ-002GKO': {},\n", + " 'name': 'FC Bayern München',\n", + " 'id': 'DFL-CLU-00000G'},\n", + " 'home': {'players': {'DFL-OBJ-0026ZI': {},\n", " 'DFL-OBJ-002GBW': {},\n", - " 'DFL-OBJ-000270': {},\n", - " 'DFL-OBJ-J01BGM': {},\n", - " 'DFL-OBJ-002G89': {},\n", - " 'DFL-OBJ-J01D1W': {},\n", + " 'DFL-OBJ-002GKO': {},\n", + " 'DFL-OBJ-0002BO': {},\n", + " 'DFL-OBJ-0027V2': {},\n", " 'DFL-OBJ-0002HE': {},\n", + " 'DFL-OBJ-00012X': {},\n", + " 'DFL-OBJ-0027B6': {},\n", + " 'DFL-OBJ-002G4A': {},\n", " 'DFL-OBJ-0027AX': {},\n", " 'DFL-OBJ-J01DVC': {},\n", - " 'DFL-OBJ-00012X': {},\n", - " 'DFL-OBJ-0002BO': {},\n", " 'DFL-OBJ-0028BD': {},\n", - " 'DFL-OBJ-0027B6': {},\n", - " 'DFL-OBJ-0026ZI': {},\n", - " 'DFL-OBJ-002G4A': {}},\n", - " 'id': 'DFL-CLU-000008',\n", + " 'DFL-OBJ-J01D1W': {},\n", + " 'DFL-OBJ-000270': {},\n", + " 'DFL-OBJ-J01BGM': {},\n", + " 'DFL-OBJ-002G89': {}},\n", " 'jersey_colour': '#F4F5F0',\n", - " 'name': '1. FC Köln'}},\n", - " 'referees': {'referee:DFL-OBJ-000141': {},\n", - " 'referee:DFL-OBJ-00011J': {},\n", - " 'referee:DFL-OBJ-00011W': {}}}}" + " 'name': '1. FC Köln',\n", + " 'id': 'DFL-CLU-000008'}},\n", + " 'referees': {'referee:DFL-OBJ-00011J': {},\n", + " 'referee:DFL-OBJ-00011W': {},\n", + " 'referee:DFL-OBJ-000141': {}}}}" ] }, "execution_count": 7, diff --git a/examples/idsse_warehouse/catalog.db b/examples/idsse_warehouse/catalog.db new file mode 100644 index 0000000000000000000000000000000000000000..0d807f53775e89c3908b6fc56b90ad562323c55b GIT binary patch literal 20480 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 literal 0 HcmV?d00001 diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/748d49d5-48bd-40a1-aac5-ac3df2728952-m0.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/748d49d5-48bd-40a1-aac5-ac3df2728952-m0.avro new file mode 100644 index 0000000000000000000000000000000000000000..8f485b4390f4391188ee81efa5e3affeabd858ca GIT binary patch literal 4508 zcmb`KTWl0n7{{S4cw4Ivq`VMwG9k)kmd>TyZR?8Y|=ElXXpIC zZ+_q9d}pwHaD8jW*GEVd|JQOvpc5_;dIX<(fldWcsF=hue4%IPh+rB*k1Pr{uCgbp znCc@Mtci&|racVbJ#r{8Jpzw|3YiqctxCLAcd9O-CN&*@c>?SjCei}Xi_%eG0)khI|k8P4N|c%C9S$+Q?T+R^Rtcr9{TXX%Tb6BVAdTE z_@g*T2%;2*2SEfe4gC05?mUv<;V{BTFl{PKiYT$e#;!l&P#%+c(Qunw+cqTFfn_mS z%Yk5*-~;%K1B(hhi2an7giIi|PBc5HmWfyAjk;FZifIweUsAhct=c>@n1MQHm73s6 zLogr2Iw4t2_{O8a7&lW2(YA# z%D|H)g}qKhcyCmqhy@zaz_o}NzBWc>>=Oe45;{T{>#&#+E9Nwku=F(D3808TJn91w zLs?3sbyy@d>jS(DkW)dSf{)D57-O6^!KwS^VWOF~R-T}wzLsSvnMb@DYlika6kCXC zAg000gc_;x)Ez6R+8TRep?Vjn&G(A9=xdpeJr7T?S2roCLi>Y05;c}uUy6b-D$99o zOqRQ1R%}P2S&b5|6)CXR1ky>Waz5qx7`6glq0yG;{Zo6oCQB^zmNRVX*xJvJHUuY^ zD@>MPJ>9t2LRf|U*kvIK=^)cU)PX++l(U5VFid|m9WL%rm=Ft5c7yi3qYXgW4JZ{U z2DEyV-K$5bNR39xYe$5{)UCjPzQGRF8-ydZ~Jhf29)V|z)MYQB-#s&)ZPwR`O_)vlEHD=K`oyOA*2#K8ttN|j*@R2Th0n`R%wWV z>pmJ{Lk}i}byM{*x~Z~EH(eB>+Q4?oSuF+;4pl9>>%>ViKhUd*!7c8XLg;G=lJrqW z<7>9=O}Kn)1bGz6qq60c(;@U-%cPo9srZC0CLg@{rAM@n-{UQn)6-aOdx;gYJ01+%uqf*v&lMeL5C9CHVzpsfZU|g;~}0 zh|ey)a7_DP0S77XQ2X#?;sJ*oiYo zzc~8aiJs<T#f{P@@2vk0!&jv8AE7Zx8eR!J(B+cyP~6l literal 0 HcmV?d00001 diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/eab6cbfa-fef9-4779-b2e1-c63290db081c-m0.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/eab6cbfa-fef9-4779-b2e1-c63290db081c-m0.avro new file mode 100644 index 0000000000000000000000000000000000000000..5b886573fc418d457dc289087f4f0e56b80696f1 GIT binary patch literal 4507 zcmb`KPfQ$D9LEbZjaEbt1TQ_jOo+B+w#*;?d-0%6cVVHn6%%nYc{}qKrp(SvXWl@t zau6=0G5%}PUOdv`rHQE;FJ446q7>VPvG9^#Yv=qEIr4W%xqh$XUTOgg#jmY+Pnf zlrh!EG*}Z8drW&2zI)_MV0r`|2Ng0chFg_*tL~ItLQQHq_QM29*z#iqHCFHm_IJ3s zBi>vl6dRag?KEgS-2H+mgm^LxfFuG3Vp9{qco|Ly))}C}^Z^%p;cI1lm*{&(3PO4X zo~)M+lpGJ>WhRK{1A%&_8Bci55#2F}-ei!Hg(+#$9h-uc$C#fj{P)m@cb<AXW*`?4mF~5DR19EGg`DBErX`5=AV~hz71j#PGE-Dq^1)2$0Y*!dQpJj94+Jk%XnE=}rJe1maO2 zfEdbBBCW$BsaPN6Wq_O#3Ke{0hQ=7O0G`PlRD6nk}(k}|YE=p#{Msr02N2;;Kc ztxd>sPt1z#NHnW)!nGm=)|x;%NmcGnc|L)yfLCa=C3^qVo}TFf3%%uxnmV>l@uLmF z$>j>u1z1lvF18R>VII3IL?Iny8i+dZ$AEH{kROKWkEg@M9SRd-Bg$UTUU#$xD0=~= zBE^7qj>C>5#JD0%IOkeIp^7|=KP;eoJipg}yCX3LHhZ7aZXqR~A%U`1wtmE8kY zDn6Yrl*e5T|2g%72>SY z5ChkJFvOZ3ObY9!>LYYhTuLVfb z2OW(s*}6C7^05)*Q6!IwmQzfJ(0479YEG%-6S|=g)Wj?oo3Bg|@o+&bQif|#^tmhHNEJi=udi+{@t|J?Y(S!jPvm81{#-1jTl&u-qn{ZwW!bNE*K!S9B0 zxlAsqG=zPb2Z%catti!BR_oxk0E_Co9E(%s7!T4z4}efIH&r>}Qj&*uk}{=r=TTE0L3 z$y%q}{rOs6$#t&fyVS0$9mlfD$I`&9wqxR9F`xPQ3Ab^7WAG&IYyNuaOFH}hrI{-; zzntIS@ZMKTi%a|FUl`aYoqwzDTZ}(AbLOub_kzY7_Y|q={{5ehe>8k<=vv#dxIB;< YzFPn1+{^Qg9gT+2u%~V0^=cyj177c>r~m)} literal 0 HcmV?d00001 diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-2148665455330551612-0-eab6cbfa-fef9-4779-b2e1-c63290db081c.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-2148665455330551612-0-eab6cbfa-fef9-4779-b2e1-c63290db081c.avro new file mode 100644 index 0000000000000000000000000000000000000000..6d2c4aaa48421ea2e625fd10d239cd075c8e2801 GIT binary patch literal 1947 zcmb7EX-E`d7*5FwitaWGk!>mo+RfdancY>2jvrBBbW33}e9nB^PR-TKx0%>hY(+|i z-3JDRQFhuP7(!A)Wp@;0K_SxdqZT2{=%VAB)!9ZzxBB~i@AEwG`@F}zf|W@FY?=fV zEhr-=&t-Vrl;7*6DViop7frjpZqGOcs8B-APGLSD<#PE*kH*_aoe1zy;57V}S4>DijQxs0;sa1ai~>+F9)$D9VYJJIQ3j8OR9;%4hj22h z6X(b*KsX!wl7+MFg**z{XD+hG^dS*;*t^jXptPbJ6}L%6kg-N5H1-Vfx(=f$kY#XY zMkz*?9uuCIP-i$RO9*@373B6M0vQcp+8ba2(DCs+D@eeQ*g~U%1O#RettrIkR_i)GF4OGg5mHDHaKxX51*!^)bPV=T)Xzo7GWzio zkD#b;@So_!>#a{e;3W-@VN9j{1{jQ4rXeVhWdQ=ox;*W0{DXsI7o-F2*7NB&4)H1o zgC-w;;==?&0>bI)>YbKakPD&86yVXcth3v6bWFTO2x`P%SVM}^Bdh7i`~#~lWSmHq zcKL~L_%GV4U=bri1UP_z#VV80|0y27&-x}2;a=y?<{z$(^sgUQ>Q8z6a?klbM|TsAH#FZe3_P`XaNuZaFBc70T1^ z2A;j`7p)@h&VkN1TZ1QqgB&~h=CYaFuAE5!RV6q!6T4=N4L(k*px)S%KTOX6*QSiW za__+W;;5f&c~bK#r}9YU#{<=6TN~y+UH!9q>4>I5#~T(oH!hA0UXYetHe^gIJh<~= z>9)0{SJU3_x&1l%>G_YAuMZ!*YvuQzDmjx{ku#xWa$C}Zn;SigvuADE-cUbu1KFJa Y`A1Y*zpk}y-6ik44N1F?-R^49Z+AeP`Tzg` literal 0 HcmV?d00001 diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-2774007322244479359-0-13c097fb-ed6a-4e53-95aa-61c36e19e9b5.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-2774007322244479359-0-13c097fb-ed6a-4e53-95aa-61c36e19e9b5.avro new file mode 100644 index 0000000000000000000000000000000000000000..f90f7b47ea93cf3ef3ea10fb7d384116a27e90ce GIT binary patch literal 1902 zcmb7ETSyd97_QCkT4H8G64MaU&6u6tb#}Et2qnW5jb7SfJkFfWsky8(XIED-NRYyy zDC)@+qNoTGONtE2jZEnw=^oaL(G|;}#3G~1nbp}wM^}42=l{O%|G)oIRJM0!e2OLm zzgDiG41rIl8OBLbjDx0Wr_;%J94^l)KTx5JGR$Evmm||nQMAWFGj@lYVKOsUXs{vx zWe#S@0jUhCHag9xs1iUKL8xkiBHQR>5LA^7oWetH5f6PLKrkipq;cg?0xipGNEGrz zGK(Np$SEpMt|v&J07YKIZ{8YG22$i#0QoTLD| zx#`vxL@S8F5Zgkdf*1s553MOg=T;j(K9p(pyrdiuMI7;OVS%cGkdDC~O8U7NT}C&4 z5)hR14gMD$d%gAP2ZF5OF^s5;+yH~oV;X`oMG+y8t;;h4$3HkYazRGXc0Hfb;}ETa zG-!6?Pkfj_KtwoQU46__i%KO_*)lwumUXt9j*f{pgrG)F!5Z-UCuKDqnQ5@bygSv!MTbG4{>>DN=Or#+z)w~H^wbh;D<7s;@QwA=-ol)NZd-yg+39_7C*eY$%^7Y< zY_~m1K65N@_*%u|A=H`btyY`2^{mZ3f|hU0KIa+=lHGNiZ}xY!y(kUud}%LbJ_m1f zg8qALJyk_Rv$OUNw4eVGo)drL(%c2tJFc{!Z7<||R}2$x_-Kw4lS$T*-Wo$D09kWII?AXkA6gS~}j z1vUhvht?EwbE~f&ADWoG7RuOo9I?ExK+{0Nz+ew0<341UF@T>01SMmGm!s|1+n9bJ z$T}Xwv`Xd%n2bSU2qKCiLLgh0Cj-YnI5>SlvS`N0Cwm-nRZu2PEB?fX3B*N&(>2sr zEVZa~LXC~!(X^~HLmVA~H-Vr|ZNM5=)pc2kBeMxsL&!XlnwJhxsniDA7hr;wAObu< zz+x3)^nZ%y?z6E;RBFq@d~bMeKpm{z85~{ubn}j5(ou7bDZE4%T^;^2*f5;0`ew)P z9i8c^YOJ5>ogC^L|Jd|=tShhPN{PB>f8C3(ON-x=kL&#-`Q6tK&-Lq1E8gCEbh&5m z?+?>A%JO@(2cM;W`rHAjH2iF$Ayn0RqI~36UvFOZ!};0w`?_B`f4mBhv)jMbof>eu4BajWjfR`=zd76Y`pdh4=_ARO&@S%h)ZdF^$Jo0Cr3+If;^Ohung0U{ CD`4pW literal 0 HcmV?d00001 diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-eb9e2be6-0ccc-4e61-aa83-a184ad41f6f1.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-eb9e2be6-0ccc-4e61-aa83-a184ad41f6f1.metadata.json new file mode 100644 index 0000000..c4a2b91 --- /dev/null +++ b/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-eb9e2be6-0ccc-4e61-aa83-a184ad41f6f1.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"fff6c07b-7d52-4453-853f-06f13b09d84d","last-updated-ms":1775658762742,"last-column-id":12,"schemas":[{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":true},{"id":4,"name":"frame_id","type":"long","required":true},{"id":5,"name":"period","type":"string","required":true},{"id":6,"name":"timestamp","type":"timestamp","required":true},{"id":7,"name":"entity_type","type":"string","required":true},{"id":8,"name":"entity_id","type":"string","required":true},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[{"source-id":3,"field-id":1000,"transform":"identity","name":"match_id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"snapshots":[],"snapshot-log":[],"metadata-log":[],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":0} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-bf153c68-f126-4491-bfa6-00cf45343e37.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-bf153c68-f126-4491-bfa6-00cf45343e37.metadata.json new file mode 100644 index 0000000..683aaca --- /dev/null +++ b/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-bf153c68-f126-4491-bfa6-00cf45343e37.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"fff6c07b-7d52-4453-853f-06f13b09d84d","last-updated-ms":1775658790066,"last-column-id":15,"schemas":[{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":true},{"id":4,"name":"frame_id","type":"long","required":true},{"id":5,"name":"period","type":"string","required":true},{"id":6,"name":"timestamp","type":"timestamp","required":true},{"id":7,"name":"entity_type","type":"string","required":true},{"id":8,"name":"entity_id","type":"string","required":true},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":false},{"id":4,"name":"frame_id","type":"long","required":false},{"id":5,"name":"period","type":"string","required":false},{"id":6,"name":"timestamp","type":"timestamp","required":false},{"id":7,"name":"entity_type","type":"string","required":false},{"id":8,"name":"entity_id","type":"string","required":false},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false},{"id":13,"name":"vel","type":"double","required":false},{"id":14,"name":"acc","type":"double","required":false},{"id":15,"name":"entity_key","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":3,"field-id":1000,"transform":"identity","name":"match_id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":3805096703135495509,"snapshots":[{"snapshot-id":3805096703135495509,"sequence-number":1,"timestamp-ms":1775658790066,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-3805096703135495509-0-3aa2fe75-8006-42c8-97d9-5e4835aa023e.avro","summary":{"operation":"append","added-files-size":"42344336","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"42344336","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":3805096703135495509,"timestamp-ms":1775658790066}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-eb9e2be6-0ccc-4e61-aa83-a184ad41f6f1.metadata.json","timestamp-ms":1775658762742}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":3805096703135495509,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":1} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-d345f9cd-ab81-46c9-92b2-74595885722c.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-d345f9cd-ab81-46c9-92b2-74595885722c.metadata.json new file mode 100644 index 0000000..87e9084 --- /dev/null +++ b/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-d345f9cd-ab81-46c9-92b2-74595885722c.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"fff6c07b-7d52-4453-853f-06f13b09d84d","last-updated-ms":1775658816184,"last-column-id":15,"schemas":[{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":true},{"id":4,"name":"frame_id","type":"long","required":true},{"id":5,"name":"period","type":"string","required":true},{"id":6,"name":"timestamp","type":"timestamp","required":true},{"id":7,"name":"entity_type","type":"string","required":true},{"id":8,"name":"entity_id","type":"string","required":true},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":false},{"id":4,"name":"frame_id","type":"long","required":false},{"id":5,"name":"period","type":"string","required":false},{"id":6,"name":"timestamp","type":"timestamp","required":false},{"id":7,"name":"entity_type","type":"string","required":false},{"id":8,"name":"entity_id","type":"string","required":false},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false},{"id":13,"name":"vel","type":"double","required":false},{"id":14,"name":"acc","type":"double","required":false},{"id":15,"name":"entity_key","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":3,"field-id":1000,"transform":"identity","name":"match_id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":6133585602095476169,"snapshots":[{"snapshot-id":3805096703135495509,"sequence-number":1,"timestamp-ms":1775658790066,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-3805096703135495509-0-3aa2fe75-8006-42c8-97d9-5e4835aa023e.avro","summary":{"operation":"append","added-files-size":"42344336","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"42344336","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":6133585602095476169,"parent-snapshot-id":3805096703135495509,"sequence-number":2,"timestamp-ms":1775658816184,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-6133585602095476169-0-b2eff7a4-107d-40c3-846c-6d69ba0f72ff.avro","summary":{"operation":"append","added-files-size":"39125719","added-data-files":"2","added-records":"3832763","changed-partition-count":"1","total-data-files":"4","total-delete-files":"0","total-records":"7919839","total-files-size":"81470055","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":3805096703135495509,"timestamp-ms":1775658790066},{"snapshot-id":6133585602095476169,"timestamp-ms":1775658816184}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-eb9e2be6-0ccc-4e61-aa83-a184ad41f6f1.metadata.json","timestamp-ms":1775658762742},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-bf153c68-f126-4491-bfa6-00cf45343e37.metadata.json","timestamp-ms":1775658790066}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":6133585602095476169,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":2} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-97d8ffab-78aa-49d0-8fa1-f6314d7b0ee9.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-97d8ffab-78aa-49d0-8fa1-f6314d7b0ee9.metadata.json new file mode 100644 index 0000000..f469351 --- /dev/null +++ b/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-97d8ffab-78aa-49d0-8fa1-f6314d7b0ee9.metadata.json @@ -0,0 +1 @@ +{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"fff6c07b-7d52-4453-853f-06f13b09d84d","last-updated-ms":1775658839577,"last-column-id":15,"schemas":[{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":true},{"id":4,"name":"frame_id","type":"long","required":true},{"id":5,"name":"period","type":"string","required":true},{"id":6,"name":"timestamp","type":"timestamp","required":true},{"id":7,"name":"entity_type","type":"string","required":true},{"id":8,"name":"entity_id","type":"string","required":true},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":false},{"id":4,"name":"frame_id","type":"long","required":false},{"id":5,"name":"period","type":"string","required":false},{"id":6,"name":"timestamp","type":"timestamp","required":false},{"id":7,"name":"entity_type","type":"string","required":false},{"id":8,"name":"entity_id","type":"string","required":false},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false},{"id":13,"name":"vel","type":"double","required":false},{"id":14,"name":"acc","type":"double","required":false},{"id":15,"name":"entity_key","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":3,"field-id":1000,"transform":"identity","name":"match_id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":6127150891889527239,"snapshots":[{"snapshot-id":3805096703135495509,"sequence-number":1,"timestamp-ms":1775658790066,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-3805096703135495509-0-3aa2fe75-8006-42c8-97d9-5e4835aa023e.avro","summary":{"operation":"append","added-files-size":"42344336","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"42344336","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":6133585602095476169,"parent-snapshot-id":3805096703135495509,"sequence-number":2,"timestamp-ms":1775658816184,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-6133585602095476169-0-b2eff7a4-107d-40c3-846c-6d69ba0f72ff.avro","summary":{"operation":"append","added-files-size":"39125719","added-data-files":"2","added-records":"3832763","changed-partition-count":"1","total-data-files":"4","total-delete-files":"0","total-records":"7919839","total-files-size":"81470055","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":6127150891889527239,"parent-snapshot-id":6133585602095476169,"sequence-number":3,"timestamp-ms":1775658839577,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-6127150891889527239-0-236c935e-8816-455f-987e-f46e735afb70.avro","summary":{"operation":"append","added-files-size":"35081294","added-data-files":"1","added-records":"3430350","changed-partition-count":"1","total-data-files":"5","total-delete-files":"0","total-records":"11350189","total-files-size":"116551349","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":3805096703135495509,"timestamp-ms":1775658790066},{"snapshot-id":6133585602095476169,"timestamp-ms":1775658816184},{"snapshot-id":6127150891889527239,"timestamp-ms":1775658839577}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-eb9e2be6-0ccc-4e61-aa83-a184ad41f6f1.metadata.json","timestamp-ms":1775658762742},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-bf153c68-f126-4491-bfa6-00cf45343e37.metadata.json","timestamp-ms":1775658790066},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-d345f9cd-ab81-46c9-92b2-74595885722c.metadata.json","timestamp-ms":1775658816184}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":6127150891889527239,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":3} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/236c935e-8816-455f-987e-f46e735afb70-m0.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/236c935e-8816-455f-987e-f46e735afb70-m0.avro new file mode 100644 index 0000000000000000000000000000000000000000..8342db8231c337a58eb919b18ccf99bf3673bba6 GIT binary patch literal 5590 zcmb_geQ*@z8BZ`wqnt6&7?6p>>w;md7kj&x%Uwce42VP~1e^prDsx`n+kG$DvS02; zAm~}H(2@{PL4r=znd&5UM)<>0L>+ZNhX^gGjG#_CR#Y$u7;2q%rqdSe^X}&MZZF9- zi8qtUBzyaOyuau9{hsH&Rq>w}m-!67gUI-svWFP6OCwAJW0<*PU8)Cm|Ruw;;=-|@h?9t?twNg4-T zV;P%#$D{(+y0!@&j^MGC)O;z2TuY7UqB@Dg*149ZC=8Fc@vU~X-kcBRyNaHT@yFTw9|%(8)e$4 zlSmmeLABx%$&+EGg)q-g!Yfs%TS%}aPrsToaH zX;^o-SYv2}-&G8BHB?LIiERq!O}`Hm4g{b)rj8XOq3SYF*>P*qlt#$yvMU*7ysp9D zv_KU##AZz=(GB4LG_C@7>gsL0DiFR*APEVZq>NV;6Vfdeu!$?}&z-~U|4CY95VQiX z>cA)D>I{O+og@&55H>Bt{nS1R3c$;bNFhd4OeX0^Sr!V`j1JYbyLH5=W5#C8)-Z}C zLD>;w(>@A+K+f2bOr`u6e6hoB<{Fr4B7dicbXdxRAt-T+qI!}KgXeR(Pr*1YH8a6v} z13{~1%AF>P8*7EiV>*D)J*~^pI??Pv%i56LWyISTwtpw7Q4z8_gN{90*xU$HI*rSi zQL&~_0hfvZM25u3X0Z!Rrlm(tFh*0;M`>p1Y=EN(rHgiVT0Ri%9b{UXMEnkt7{#$c z1OzCX9fX$85gJ5=#*4DU<`}P9z{}ipvzX%WE)#vF%`BuQkmJEXq7#gk%^Silq#vxc~!DxB}U&E?L7i%kI0V;dWK9` z*D=T_D3eKKkYvTVe~idtmppFJhqaA}@pHvt>=WZsD-%O-s~ zX!`sfn@BQBOE0PIB9Eoj_QHo48EZ^3N{iyrLi)fVb_>>Mi5zs0C?HdX9}1|ACG>|N z8f(4fvJ-L|VlKY1v*K%t?ux#Ne%70wwIwpw00?Mh4)mR|H zP>spN#B|Ydn`GpU5X{;I-RaIaxkjN$#~Y1zD!ksM*_$Z1M``ycE~#;E2@OpW&6t`< z7{nY!gkaWs=!Va15bkn8BqGDjDxEaltkV02vs&ey<83qr6T?jx==^lTKOuzd@1aznobOQA%`(-KnS?qKnQyGflXO7>{OhU zD!uXPuyotCNk5RWA|~K_p%}Q)yO?CrH@p9OV)%5hdh4G~E!_LYPj^f^TFS>Sta;?j z(NBA~-?(Sa4RhvP2@H^-{`=2gytt+Eh05zM&aU`!;mre;1J&)jDlWd>zk6Wl?Ck!^ z^G{aXT;1M2G;rjF1-lo#v+&%xz6FV);=#6CXBji}*4}>x+qU+$ojLZy-qzr^&An}( z&%N8au&L*h!#mGz``7V`HM7I>qOl|8(H~!RM*GEe$*amwgieIGy5TV2ePi;!|rnaOWTfA}cSMMi_cg*|G^@AJljlcC>&+*pAU;E0B!3~NH zEdTv;UEA+yY<#xZ_v+FQxZbVJUj%0vAFMAalb&jO{>n3bsmJEu!Y*to+I#HuADd@a zy!g({B~?9N_Vh7TKHq~|4oCidUC)g>i}u|xx4#4>*xI9|51;HlfBQe~xw7$@X)k{I z8{gjdjt;l~GIGanBlB-RxZvRa%}pEe$!FEMKWp06Up;h9Y3i;$+y48>>YK{SuRd^k z`L;8-@2MwBnpS*Q(R54dqmNUI&R(isbZB_p<4>g4?kS#mNc-%w^?w)+o!fDF)#g>n R%kyXKuPFNJcNcQo=6|{bEo%S( literal 0 HcmV?d00001 diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/3aa2fe75-8006-42c8-97d9-5e4835aa023e-m0.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/3aa2fe75-8006-42c8-97d9-5e4835aa023e-m0.avro new file mode 100644 index 0000000000000000000000000000000000000000..091dea3c22ecda2c1fd843d44adc425d51d4d29e GIT binary patch literal 6126 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 literal 0 HcmV?d00001 diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-3805096703135495509-0-3aa2fe75-8006-42c8-97d9-5e4835aa023e.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-3805096703135495509-0-3aa2fe75-8006-42c8-97d9-5e4835aa023e.avro new file mode 100644 index 0000000000000000000000000000000000000000..627db9cfed8e131be4823db53cf8cab2aa7bfaba GIT binary patch literal 1851 zcmb7EZ%7ki9M(l>Y9%CUrLwLv6&IUxI+Y?QEx{7X1X*&vyL-1?-Q7Fy-nquy7b_I9 zAc&wyB8#YqiqQH}AO=xkAS9w@(1%1Z0u%bshyJ{`?dIv}w2$}R?|Giz@A>_ycb-m6 z%aldnl|2ZXI5yvEH`~k=W#wk8#cC_9u#t~lULZjcn>1m`qL1hEWZ3G1A_GmLPjEuX zV99YINdVZ?1|^w8qM;-Mv`J_$gILIHVxfx%7-oi_WX1yp@Hi_-;eZ!Xl}1W}43b!? z*hslJ$g?tea|9_72;sj15IGl=G3^D|6AK~08Fwm zo;+S_^mEu_tT|x}>q9D3Y+OczhoXBK6^llNM?_;0nwW$+MTeHXkfBLtYAI@#B^o>@ z;>B|t8=kj&DJ>^tW( znn6B&`Q}m$UgsG+%gLBDn~ukz^+3nKLLOpLLOcSk1v-L=0}}GDP=H0-bbSkA6~tkP zZ=qU290IL}`V?YwD=!@%vu5@jl(O+8Vqsx{B!Pf}As!0KeTXijojh?E3d#mAM8{um zW%>ap%47_~Dv=wYGFE7Yz=;qKfv8`e2%P*O!Ql%MMO&17qQ@at1*Ot7lb_@;0UwV^ zx{CUurRI?zO0<)Vrf!`rnxmuP4PYo!tFZdK-W6FjM`jJIijaCDC7an!g+i-npMe2d zfEcg<1D#bGqyJMpcAu3^qCyExn+qQHBy@iszE*j-uVrq)@W;@6u_|ToKt^@vOv&V+ z{owu^V;h`1CJHAmxaWJ%)#eX%-?ELnlQJ7SI(qt3)3aLo(yxBH8T^v-=+NCU(0#PD z=tIixX}KZWbjh`EvZW}sVW^<1k)qFAvXggS-<}?rYE5do((9r|x4kwMoO<8?2F*F1 zrEP2t~1OER&XBl$%(hTb5c>oSC1;WF(SU zR+O)ooS%}K%$JgymXlbLDh3uRPR>ZpOQ&3BwKRwfp(zi28LNlCdi)n z;)2xVc%aK*UV*vN2uXitUJ0u9K2A)5W=&K(|7p z5SIyNcuh!5Nl8tKhb0qiel{jTe_CcvYH@sWekrE+4NQ>qV}x01MP_jcFtX$EnSdMy z7$&5o=A@PYy@1aE01^U4`QY(`_+& z@-aY4d(ldHr8zl36^IO*SX7i)3DN@$!(32t0nLmSK%vZ%)Lc*wMyST0y+NrzF*C0i zm<++TK*|7^iIzx-ASpjTCp9q-GxI>S17(1U!6gV(w;{+q(82-k3niE_u|P>+!IW5< zQv!4@$aa)Wos(alS`?oIOqv+k*$}DFK{B_pB(+$HfT^Vg1vpJbDrAT-6=V^th%5pY z;!3r(1nkaEt&Go2ElEsCEJ?%&Dx`oW(Qr+3;NYdbd!GK4|I;qNK7YlI#_lPX){2yR-L~Rnx*~CH^OUd+ z&Am%{XQf58?Jmqd-k+lO*B~?1DDP>x&4jaM%)wIbnO*<(U5W7j7W7B^PyQvfv`^)R zJB6$6xINmH+8(iOZO3yil~Y9l1<|V<3@)W`Z(2P0w3NfjlA4c~_Djmhm2$HkczOG$ zOhclB-RG%KO@m@@sjTj~ZWg9y>`?D^?10PCmMtDLJ3AtDo*daaXM)AV$E=ey6SYju z;(jw6eRT5b;kOw-3a{ykFz&lIf0^|4(~~R9-1L^N47hP5RpUc2SOt8Y)1d?4*2^AtT`0qpd$z5oCK literal 0 HcmV?d00001 diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-6133585602095476169-0-b2eff7a4-107d-40c3-846c-6d69ba0f72ff.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-6133585602095476169-0-b2eff7a4-107d-40c3-846c-6d69ba0f72ff.avro new file mode 100644 index 0000000000000000000000000000000000000000..936c6d39630c99ba194a41e187406ea2acb0dc6e GIT binary patch literal 1936 zcmb7ETSyd97&cuYOX|gxGHM&TNh&kD&N^F5U{sJrkyenGrn56=b#gB2%o%N~eaMO+ zGopenJ{0(%z1Z!cpc`E*>}Cco_F(Blgi$05Qgorttj;z%y4B;H`M>Y`|L?zS^6#3G zo~g+os8uT{Pv94OD7TwtXbvmLhw^wA27wA?lxGcdGbBxVJ%yy3a?=G~8h^~w zV66^i4(7?aYMC4lnkpsERqob1W~b*hrjDLmw6@Gu|(1T$k#8dnV^(4MuI zK%pQcmOBX*a*E0mxlSS=K#|w*Tlro>22$*=1Y{uqHN*x1s*VTY{Bamfk{D(3Xh;?0 zDkFqfID>eD!U2S{ac(VH=d2e{wR3H$Go}xTaISL#4H3#!6R1!Y6;Z(&W6;<$Bp5oZ z7K9v&Gc!vuvvgVTf{ez(IYma;^YI|6FcHXX06WqE8<@6$?HNk~&ULhflQDP_G60^3 zJZp~0L^Zqr0Ur=VsIi=)+sh{lEO?t|aJQf#+-x=xgVh5Y0}n-ra0!VBtQOb^MjTL; z`bi3)tWCGKAYMTNhQt<{6(k_AdT37}KDXNV@gXgA*d05Vbz1dNm(sN<{wxM zA@fA4G|3R*@L#lRV2G6<0z5##W|hV0{}hkkXJeCyaMH^x-;Ji;ot@W~wSRhYq4kF2 zh@+$Hc5zBkMrwa;-{YZKa^UK(nkA0u)V3paPy1f42-N;Mv%RHjAmwY);DILSShl7k zUoTEgi?la1+}j~*UHeP-x!1o-mDCqA(}f4wm906#iI&LFl%|Vq*{^Q(EZr3Sz;s^j zSRL8g80r~D$6)F4flwfQQB|9;<@}s)vw!S}o+$gVr{wUdY263zOmz<(L<^$HX+@b2 zbEeH&UR;#3hlvd9_s>XdMc>=ptV^d)(#==CoNm=0Zqsopn-1|lmzPW=+Rbobos z=AS3K*Px6;D_5^_^m|dTjU2ecziIU)l4c M;iSQ*XUF^U8 None: NestedField(12, "z", FloatType(), required=False), ) - # Define partitioning by match_id - partition_spec = PartitionSpec( - PartitionField( - source_id=3, field_id=1000, transform=IdentityTransform(), name="match_id" + # Build PartitionSpec from config + partition_fields = [] + for field_name in self.config.partition_by: + # Find field in schema to get its ID + field = schema.find_field(field_name) + partition_fields.append( + PartitionField( + source_id=field.field_id, + field_id=1000 + field.field_id, + transform=IdentityTransform(), + name=field_name, + ) ) - ) + partition_spec = PartitionSpec(*partition_fields) + + # Map config to Iceberg table properties for optimization + properties = { + "write.parquet.compression-codec": self.config.compression, + "write.parquet.page-size-bytes": str(self.config.page_size), + "write.parquet.row-group-size": str(self.config.row_group_size), + "write.parquet.dict-enabled": str(self.config.use_dictionary).lower(), + } # Create table self._tracking_table = self._iceberg_catalog.create_table( table_name, schema=schema, partition_spec=partition_spec, + properties=properties, ) def _create_metadata_table(self) -> None: @@ -192,9 +209,17 @@ def _create_metadata_table(self) -> None: NestedField(4, "metadata_json", StringType(), required=True), ) + # Metadata table is also partitioned by match_id for consistency + partition_spec = PartitionSpec( + PartitionField( + source_id=1, field_id=1001, transform=IdentityTransform(), name="match_id" + ) + ) + self._metadata_table = self._iceberg_catalog.create_table( table_name, schema=schema, + partition_spec=partition_spec, ) def load_container( diff --git a/python/tracking_data_container/config.py b/python/tracking_data_container/config.py index bb7093c..b7af0ce 100644 --- a/python/tracking_data_container/config.py +++ b/python/tracking_data_container/config.py @@ -8,8 +8,7 @@ class IcebergConfig: """Configuration for Iceberg table optimization.""" # Partitioning - bucket_count: int = 10 - partition_by: list[str] = field(default_factory=lambda: ["match/id"]) + partition_by: list[str] = field(default_factory=lambda: ["match_id"]) # Parquet optimization compression: str = "snappy" # or "zstd" @@ -22,4 +21,4 @@ class IcebergConfig: use_dictionary: bool = True # Sorting - sort_by: list[str] = field(default_factory=lambda: ["player_id", "timestamp"]) + sort_by: list[str] = field(default_factory=lambda: ["entity_id", "timestamp"]) From a507767bc0495b9a63360819afc15f7cdf3444e7 Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Wed, 8 Apr 2026 17:05:41 +0200 Subject: [PATCH 19/21] refactor: use TrackedEntity objects and simplify catalog logic --- crates/tdc-core/src/datacontainer/core.rs | 34 +- crates/tdc-core/src/schema/field.rs | 10 + crates/tdc-python/src/container.rs | 8 +- .../_tracking_data_container.pyi | 4 +- python/tracking_data_container/catalog.py | 482 +++++++----------- python/tracking_data_container/container.py | 6 +- python/tracking_data_container/schema.py | 4 +- 7 files changed, 226 insertions(+), 322 deletions(-) diff --git a/crates/tdc-core/src/datacontainer/core.rs b/crates/tdc-core/src/datacontainer/core.rs index 9b1a3ce..d42c570 100644 --- a/crates/tdc-core/src/datacontainer/core.rs +++ b/crates/tdc-core/src/datacontainer/core.rs @@ -370,7 +370,10 @@ impl TrackingDataContainer { } } - pub fn pivot(data: StructArray) -> TrackingDataContainerResult { + pub fn pivot( + data: StructArray, + entity_to_team: Option>, + ) -> TrackingDataContainerResult { use crate::datacontainer::builder::TrackingDataBuilder; use ahash::AHashSet; use arrow_array::types::{ @@ -473,11 +476,6 @@ impl TrackingDataContainer { "Missing required entity_id or player/id column".to_string(), ) })?; - let team_id_idx = team_id_idx.ok_or_else(|| { - TrackingDataContainerErrorKind::InvalidParameters( - "Missing team_id or team/id column".to_string(), - ) - })?; // 3. Determine min/max frame index for fixed builder if possible let mut min_f = i64::MAX; @@ -531,7 +529,6 @@ impl TrackingDataContainer { .iter() .map(|&idx| { let col_name = fields[idx].name(); - // Map back to internal CDF-like name for consistency in wide format if needed let wide_name = match col_name.as_str() { "match_id" => "match/id", "competition_id" => "competition/id", @@ -549,6 +546,7 @@ impl TrackingDataContainer { .collect(); // 5. Pre-calculate entity attribute mappings + // (player_id, team_id) -> (Vec, Vec) let mut entity_map = ahash::AHashMap::new(); // 6. Pivot data @@ -624,15 +622,21 @@ impl TrackingDataContainer { _ => continue, }; - let t_id_col = &columns[team_id_idx]; - let t_id = if t_id_col.is_null(i) { - "none" - } else { - match t_id_col.data_type() { - Utf8 => t_id_col.as_string::().value(i), - LargeUtf8 => t_id_col.as_string::().value(i), - _ => "none", + let t_id = if let Some(idx) = team_id_idx { + let col = &columns[idx]; + if col.is_null(i) { + "none" + } else { + match col.data_type() { + Utf8 => col.as_string::().value(i), + LargeUtf8 => col.as_string::().value(i), + _ => "none", + } } + } else if let Some(ref mapping) = entity_to_team { + mapping.get(p_id).map(|s| s.as_str()).unwrap_or("none") + } else { + "none" }; let attr_mappings = entity_map.entry((p_id, t_id)).or_insert_with(|| { diff --git a/crates/tdc-core/src/schema/field.rs b/crates/tdc-core/src/schema/field.rs index e608633..798715a 100644 --- a/crates/tdc-core/src/schema/field.rs +++ b/crates/tdc-core/src/schema/field.rs @@ -128,6 +128,16 @@ impl DataField { attribute: Self::parse_attr(&attr_str), } } + ["other", pid, ..] => { + let attr_str = parts[2..].join("/"); + DataField::TrackedEntity { + entity: TrackedEntity::Player { + team_id: "none".to_string(), + player_id: pid.to_string(), + }, + attribute: Self::parse_attr(&attr_str), + } + } ["vendor", "event", "name"] => DataField::Frame { attribute: FrameAttribute::VendorEventName, }, diff --git a/crates/tdc-python/src/container.rs b/crates/tdc-python/src/container.rs index 93d9a29..7e6c338 100644 --- a/crates/tdc-python/src/container.rs +++ b/crates/tdc-python/src/container.rs @@ -349,7 +349,11 @@ impl PyTrackingDataContainer { } #[staticmethod] - fn pivot(data: Bound<'_, PyAny>) -> PyResult { + #[pyo3(signature = (data, entity_to_team=None))] + fn pivot( + data: Bound<'_, PyAny>, + entity_to_team: Option>, + ) -> PyResult { let capsules = data.call_method0("__arrow_c_array__")?; let tuple = capsules.downcast::()?; let array_item = tuple.get_item(0)?; @@ -366,7 +370,7 @@ impl PyTrackingDataContainer { .ok_or_else(|| PyTypeError::new_err("Expected StructArray"))? .clone(); - let inner = TrackingDataContainer::pivot(struct_array).into_pyresult()?; + let inner = TrackingDataContainer::pivot(struct_array, entity_to_team).into_pyresult()?; Ok(PyTrackingDataContainer { inner }) } diff --git a/python/tracking_data_container/_tracking_data_container.pyi b/python/tracking_data_container/_tracking_data_container.pyi index 113da2b..369c27f 100644 --- a/python/tracking_data_container/_tracking_data_container.pyi +++ b/python/tracking_data_container/_tracking_data_container.pyi @@ -166,7 +166,9 @@ class _TrackingDataContainer: @classmethod def _from_capsule(cls, capsule: Any) -> _TrackingDataContainer: ... @staticmethod - def pivot(data: Any) -> _TrackingDataContainer: ... + def pivot( + data: Any, entity_to_team: dict[str, str] | None = None + ) -> _TrackingDataContainer: ... @staticmethod def from_arrow( array_capsule: Any, diff --git a/python/tracking_data_container/catalog.py b/python/tracking_data_container/catalog.py index eb62f45..211784b 100644 --- a/python/tracking_data_container/catalog.py +++ b/python/tracking_data_container/catalog.py @@ -16,7 +16,6 @@ And, BooleanExpression, EqualTo, - Or, Reference, ) from pyiceberg.io.pyarrow import _pyarrow_to_schema_without_ids @@ -42,12 +41,12 @@ logger = logging.getLogger(__name__) # Mapping from internal CDF-like names to snake_case Iceberg names +# Note: team/id is handled as a constant attribute in metadata COLUMN_MAPPING = { "match/id": "match_id", "competition/id": "competition_id", "season/id": "season_id", "player/id": "entity_id", - "team/id": "team_id", } # Reverse mapping @@ -89,7 +88,6 @@ def __init__( # Default properties for SQL catalog (SQLite for POC) import multiprocessing - # Optimize IO parallelism num_workers = multiprocessing.cpu_count() default_properties = { @@ -120,30 +118,25 @@ def initialize(self) -> None: Creates tables with configured partitioning and optimization settings. Must be called once before first use. Idempotent - safe to call multiple times. """ - # Create namespace if it doesn't exist try: self._iceberg_catalog.create_namespace("tracking") except Exception: pass # Namespace already exists - # Create tracking_data table self._create_tracking_table() - - # Create match_metadata table self._create_metadata_table() def _create_tracking_table(self) -> None: """Create tracking_data Iceberg table in the requested long format.""" table_name = "tracking.tracking_data" - # Check if table exists try: self._tracking_table = self._iceberg_catalog.load_table(table_name) - return # Table exists + return except NoSuchTableError: - pass # Create new table + pass - # Define the requested schema. x, y, z are nullable (optional in Iceberg). + # Define schema. Spatial columns are optional but mandatory during ingestion. schema = Schema( NestedField(1, "competition_id", StringType(), required=False), NestedField(2, "season_id", StringType(), required=False), @@ -153,16 +146,14 @@ def _create_tracking_table(self) -> None: NestedField(6, "timestamp", TimestampType(), required=True), NestedField(7, "entity_type", StringType(), required=True), NestedField(8, "entity_id", StringType(), required=True), - NestedField(9, "team_id", StringType(), required=False), - NestedField(10, "x", FloatType(), required=False), - NestedField(11, "y", FloatType(), required=False), - NestedField(12, "z", FloatType(), required=False), + NestedField(9, "x", FloatType(), required=False), + NestedField(10, "y", FloatType(), required=False), + NestedField(11, "z", FloatType(), required=False), ) # Build PartitionSpec from config partition_fields = [] for field_name in self.config.partition_by: - # Find field in schema to get its ID field = schema.find_field(field_name) partition_fields.append( PartitionField( @@ -174,7 +165,7 @@ def _create_tracking_table(self) -> None: ) partition_spec = PartitionSpec(*partition_fields) - # Map config to Iceberg table properties for optimization + # Optimization properties properties = { "write.parquet.compression-codec": self.config.compression, "write.parquet.page-size-bytes": str(self.config.page_size), @@ -182,7 +173,6 @@ def _create_tracking_table(self) -> None: "write.parquet.dict-enabled": str(self.config.use_dictionary).lower(), } - # Create table self._tracking_table = self._iceberg_catalog.create_table( table_name, schema=schema, @@ -194,14 +184,12 @@ def _create_metadata_table(self) -> None: """Create match_metadata Iceberg table.""" table_name = "tracking.match_metadata" - # Check if table exists try: self._metadata_table = self._iceberg_catalog.load_table(table_name) return except NoSuchTableError: pass - # Define schema for metadata schema = Schema( NestedField(1, "match_id", StringType(), required=True), NestedField(2, "competition_id", StringType(), required=False), @@ -209,7 +197,6 @@ def _create_metadata_table(self) -> None: NestedField(4, "metadata_json", StringType(), required=True), ) - # Metadata table is also partitioned by match_id for consistency partition_spec = PartitionSpec( PartitionField( source_id=1, field_id=1001, transform=IdentityTransform(), name="match_id" @@ -228,12 +215,7 @@ def load_container( player_id: str | None = None, **filters: Any, ) -> CatalogLazyTrackingDataContainer: - """ - Load container from storage lazily. - - Returns a LazyTrackingDataContainer that wraps the physical long-format - Iceberg table but allows logical wide-format analysis. - """ + """Load container from storage lazily.""" return CatalogLazyTrackingDataContainer( self, match_id=match_id, player_id=player_id, **filters ) @@ -244,62 +226,30 @@ def save_container( match_id: str | None = None, metadata: dict | None = None, ) -> None: - """ - Save container to storage (replaces match partition). - - Args: - container: Container to save - match_id: Optional match ID override - metadata: Optional additional metadata to save - """ + """Save container to storage (replaces match partition).""" if match_id is None: - # Try to get match_id from first row if possible df = container.to_polars().head(1) - if "match/id" in df.columns: - match_id = str(df["match/id"][0]) - elif "match_id" in df.columns: - match_id = str(df["match_id"][0]) - else: - raise ValueError("match_id must be provided if not present in container data") - - # 1. Save tracking data and get unique entities (hierarchical) + match_id = ( + str(df["match/id"][0]) if "match/id" in df.columns else str(df["match_id"][0]) + ) + entities = self._save_tracking_data(container, match_id) - # 2. Prepare metadata final_metadata = metadata.copy() if metadata else {} final_metadata["_entities"] = entities - - # 3. Save metadata self._save_metadata(final_metadata, match_id) def commit_container(self, container: TrackingDataContainer) -> None: - """ - Commit bound container. - - Args: - container: Bound container to commit - """ - # For POC, we just use save_container - # Try to find match_id + """Commit bound container.""" df = container.to_polars().head(1) match_id = df["match/id"][0] if "match/id" in df.columns else df["match_id"][0] self.save_container(container, match_id=str(match_id)) - def get_metadata(self, match_id: str) -> dict: - """ - Load metadata for a match. - - Args: - match_id: ID of the match - - Returns: - Metadata dictionary - """ + def get_metadata(self, match_id: str) -> dict[Any, Any]: + """Load metadata for a match.""" if self._metadata_table is None: self._metadata_table = self._iceberg_catalog.load_table("tracking.match_metadata") - assert self._metadata_table is not None - scan = self._metadata_table.scan( row_filter=EqualTo(term=Reference("match_id"), value=cast(Any, match_id)) # type: ignore ) @@ -316,110 +266,88 @@ def _save_tracking_data( container: TrackingDataContainer, match_id: str, ) -> dict[str, Any]: - """ - Save tracking data to Iceberg table in entity-long format. - - Returns: - Hierarchical dictionary of unique entities and constants. - """ + """Save tracking data to Iceberg table in entity-long format.""" if self._tracking_table is None: self._tracking_table = self._iceberg_catalog.load_table("tracking.tracking_data") - assert self._tracking_table is not None - - # 1. Melt to entity-long format + # 1. Melt and add entity types long_df = container.melt() - - # 2. Add entity_type and rename columns to snake_case - if "entity_key" in long_df.columns: - long_df = long_df.with_columns( - pl.col("entity_key") - .map_elements( - lambda k: TrackedEntity.from_entity_key(k).type, return_dtype=pl.String - ) - .alias("entity_type") - ) - else: + if "entity_key" not in long_df.columns: raise ValueError("entity_key column is required for saving to catalog") - # Rename columns to Iceberg format - rename_dict = {k: v for k, v in COLUMN_MAPPING.items() if k in long_df.columns} - long_df = long_df.rename(rename_dict) + long_df = long_df.with_columns( + pl.col("entity_key") + .map_elements(lambda k: TrackedEntity.from_entity_key(k).type, return_dtype=pl.String) + .alias("entity_type") + ) - # Ensure match_id is present and correct - long_df = long_df.with_columns(pl.lit(match_id).alias("match_id")) + # 2. Column Mapping + rename_dict = {k: v for k, v in COLUMN_MAPPING.items() if k in long_df.columns} + long_df = long_df.rename(rename_dict).with_columns(pl.lit(match_id).alias("match_id")) - # --- Constant Extraction & Hierarchical grouping --- - ignore_for_constants = { + # 3. Constant Extraction (Normalization) + # Identity and core temporal fields are strictly preserved in tracking table. + # team/id is now moved to metadata as a constant. + core_cols = { "match_id", "frame_id", "period", "timestamp", "entity_type", "entity_id", - "team_id", "x", "y", "z", "entity_key", } - potential_constants = [c for c in long_df.columns if c not in ignore_for_constants] - - # Group by entity and extract constants - agg_exprs = [pl.col(c).n_unique().alias(f"{c}_count") for c in potential_constants] - agg_vals = [pl.col(c).first().alias(f"{c}_val") for c in potential_constants] + potential_constants = [c for c in long_df.columns if c not in core_cols and c != "team/id"] - stats = long_df.group_by(["entity_id", "team_id", "entity_type"]).agg(agg_exprs + agg_vals) + # Group by entity AND its original team attribute to extract constants correctly + # team/id is part of the key to identify constants per-entity-instance + agg_stats = long_df.group_by(["entity_id", "team/id", "entity_type"]).agg( + [pl.col(c).n_unique().alias(f"{c}_count") for c in potential_constants] + + [pl.col(c).first().alias(f"{c}_val") for c in potential_constants] + ) - columns_to_drop = [] - for c in potential_constants: - if stats[f"{c}_count"].max() == 1: - columns_to_drop.append(c) + constants_to_move = [c for c in potential_constants if agg_stats[f"{c}_count"].max() == 1] + # Prepare hierarchical metadata entities_flat = ( - stats.select( - ["entity_id", "team_id", "entity_type"] + [f"{c}_val" for c in columns_to_drop] + agg_stats.select( + ["entity_id", "team/id", "entity_type"] + [f"{c}_val" for c in constants_to_move] ) - .rename({f"{c}_val": c for c in columns_to_drop}) + .rename({f"{c}_val": c for c in constants_to_move}) .to_dicts() ) - # Build hierarchical structure, omitting None attributes - # Flattened team attributes: teams > {tid} > {attr} - # instead of teams > {tid} > attributes > {attr} - entities_hierarchical: dict[str, Any] = {"ball": {}, "teams": {}, "referees": {}} + hierarchical_meta: dict[str, Any] = {"ball": {}, "teams": {}, "referees": {}} for ent in entities_flat: - etype = ent.pop("entity_type") - eid = ent.pop("entity_id") - tid = ent.pop("team_id", "none") - - # Remove attributes that are None for this specific entity + etype, eid, tid = ( + ent.pop("entity_type"), + ent.pop("entity_id"), + ent.pop("team/id", "none"), + ) attrs = {k: v for k, v in ent.items() if v is not None} if etype == "ball": - entities_hierarchical["ball"] = attrs + hierarchical_meta["ball"] = attrs elif etype == "referee": - entities_hierarchical["referees"][eid] = attrs + hierarchical_meta["referees"][eid] = attrs elif etype == "player": - if tid not in entities_hierarchical["teams"]: - entities_hierarchical["teams"][tid] = {"players": {}} - entities_hierarchical["teams"][tid]["players"][eid] = attrs + team = hierarchical_meta["teams"].setdefault(tid, {"players": {}}) + team["players"][eid] = attrs elif etype == "team": - # Use tid (e.g. 'home') instead of eid (e.g. 'team:home') for the key target_tid = tid if tid != "none" else eid.replace("team:", "") - if target_tid not in entities_hierarchical["teams"]: - entities_hierarchical["teams"][target_tid] = {"players": {}} - # Merge team-level attributes directly into the team object - entities_hierarchical["teams"][target_tid].update(attrs) + team = hierarchical_meta["teams"].setdefault(target_tid, {"players": {}}) + team.update(attrs) - if columns_to_drop: - logger.info(f"Normalizing constant attributes to metadata: {columns_to_drop}") - long_df = long_df.drop(columns_to_drop) + # Drop constant columns AND the team/id column from physical table + long_df = long_df.drop([c for c in constants_to_move if c in long_df.columns] + ["team/id"]) - # 3. Validation + # 4. Final Validation & Save for field in ["x", "y"]: if field not in long_df.columns: - raise ValueError(f"Required column {field} is missing from container") + raise ValueError(f"Required column {field} is missing") must_not_be_null = [ "match_id", @@ -435,7 +363,7 @@ def _save_tracking_data( if long_df[field].null_count() > 0: raise ValueError(f"Required field {field} contains null values") - # 4. Save to Iceberg + # Convert to Arrow arrow_table = long_df.to_arrow() known_types = { "timestamp": pa.timestamp("us"), @@ -445,39 +373,32 @@ def _save_tracking_data( "z": pa.float32(), } - new_columns = [] - new_fields = [] + new_cols, new_fields = [], [] for i, field in enumerate(arrow_table.schema): - target_type = known_types.get(field.name) - if not target_type and isinstance(field.type, pa.DictionaryType): - target_type = field.type.value_type - elif not target_type and pa.types.is_null(field.type): - target_type = pa.string() - - if target_type is not None: - new_columns.append(arrow_table.column(i).cast(target_type)) - new_fields.append(pa.field(field.name, target_type, field.nullable)) - else: - new_columns.append(arrow_table.column(i)) - new_fields.append(field) + target = known_types.get(field.name) or ( + field.type.value_type + if isinstance(field.type, pa.DictionaryType) + else (pa.string() if pa.types.is_null(field.type) else None) + ) + new_cols.append(arrow_table.column(i).cast(target) if target else arrow_table.column(i)) + new_fields.append(pa.field(field.name, target, field.nullable) if target else field) - arrow_table = pa.Table.from_arrays(new_columns, schema=pa.schema(new_fields)) + arrow_table = pa.Table.from_arrays(new_cols, schema=pa.schema(new_fields)) with self._tracking_table.transaction() as txn: - container_schema = _pyarrow_to_schema_without_ids(arrow_table.schema) - txn.update_schema().union_by_name(container_schema).commit() + txn.update_schema().union_by_name( + _pyarrow_to_schema_without_ids(arrow_table.schema) + ).commit() txn.delete(EqualTo(term=Reference("match_id"), value=cast(Any, match_id))) # type: ignore txn.append(arrow_table) - return entities_hierarchical + return hierarchical_meta def _save_metadata(self, metadata: dict, match_id: str) -> None: """Save metadata to Iceberg table.""" if self._metadata_table is None: self._metadata_table = self._iceberg_catalog.load_table("tracking.match_metadata") - assert self._metadata_table is not None - metadata_schema = pa.schema( [ pa.field("match_id", pa.string(), nullable=False), @@ -503,10 +424,9 @@ def _save_metadata(self, metadata: dict, match_id: str) -> None: def __repr__(self) -> str: return ( - f"TrackingDataCatalog(\n" - f" warehouse={self.warehouse_path},\n" - f" catalog_type={self.catalog_type}\n" - f")" + f"TrackingDataCatalog(" + f"warehouse={self.warehouse_path}, " + f"catalog_type={self.catalog_type})" ) @@ -531,100 +451,95 @@ def __init__( self._filters = filters self._cached_schema: Fields | None = None - def _get_flat_entities(self, hierarchical_entities: dict[str, Any]) -> list[dict[str, Any]]: - """Flatten hierarchical metadata back to list for easy schema iteration.""" - flat = [] - # 1. Ball - ball = hierarchical_entities.get("ball") - if ball is not None: - flat.append({"entity_id": "ball", "team_id": "none", **ball}) - - # 2. Teams & Players - for tid, team_info in hierarchical_entities.get("teams", {}).items(): - # Team itself - # We need to exclude "players" from the attributes when flattening the team itself - team_attrs = {k: v for k, v in team_info.items() if k != "players"} - flat.append({"entity_id": f"team:{tid}", "team_id": tid, **team_attrs}) - # Players - for pid, p_attrs in team_info.get("players", {}).items(): - flat.append({"entity_id": pid, "team_id": tid, **p_attrs}) - - # 3. Referees + def _get_entities_from_metadata( + self, hierarchical_entities: dict[str, Any] + ) -> list[tuple[TrackedEntity, dict[str, Any]]]: + """Flatten hierarchical metadata into a list of (TrackedEntity, attributes) tuples.""" + entities: list[tuple[TrackedEntity, dict[str, Any]]] = [] + + if ball_attrs := hierarchical_entities.get("ball"): + entities.append((TrackedEntity.from_entity_key("ball|none"), ball_attrs)) + + for tid, team_data in hierarchical_entities.get("teams", {}).items(): + team_attrs = {k: v for k, v in team_data.items() if k != "players"} + entities.append((TrackedEntity.from_entity_key(f"team:{tid}|{tid}"), team_attrs)) + for pid, p_attrs in team_data.get("players", {}).items(): + entities.append((TrackedEntity.from_entity_key(f"{pid}|{tid}"), p_attrs)) + for rid, r_attrs in hierarchical_entities.get("referees", {}).items(): - flat.append({"entity_id": f"referee:{rid}", "team_id": "none", **r_attrs}) + entities.append((TrackedEntity.from_entity_key(f"referee:{rid}|none"), r_attrs)) + + if self._player_id: + entities = [ + (e, attrs) + for e, attrs in entities + if e.player_id == self._player_id + or (e.is_referee and e.referee_id == self._player_id) + ] - return flat + return entities + + def _get_wide_names_for_entity( + self, entity: TrackedEntity, attributes: dict[str, Any], physical_attr_cols: list[str] + ) -> list[str]: + """Generate all wide CDF field names for a specific entity.""" + prefix = entity.cdf_prefix() + names = [] + for attr in physical_attr_cols: + names.append(f"{prefix}{attr}") + for attr in attributes: + names.append(f"{prefix}{attr}") + if not entity.is_ball: + names.append(f"{prefix}player/id") + if entity.is_player or entity.is_team: + names.append(f"{prefix}team/id") + return names def schema(self) -> Fields: - """Reconstruct the wide schema by inspecting entities in the hierarchical metadata.""" + """Reconstruct the wide schema by inspecting entities in the match metadata.""" if self._cached_schema: return self._cached_schema - assert self._catalog is not None if self._catalog._tracking_table is None: self._catalog._tracking_table = self._catalog._iceberg_catalog.load_table( "tracking.tracking_data" ) - assert self._catalog._tracking_table is not None - metadata = self._catalog.get_metadata(self._match_id) entities_hier = metadata.get("_entities") if entities_hier is None: raise ValueError(f"Metadata for match {self._match_id} is missing unique entity list.") - entities = self._get_flat_entities(entities_hier) - - if self._player_id: - entities = [e for e in entities if str(e.get("entity_id")) == self._player_id] - - physical_names = [f.name for f in self._catalog._tracking_table.schema().fields] - ignored = set(COLUMN_MAPPING.values()) | { - "entity_type", + physical_names = {f.name for f in self._catalog._tracking_table.schema().fields} + ignored = { + "match_id", + "competition_id", + "season_id", "timestamp", "frame_id", "period", "period_id", + "entity_type", + "entity_id", "entity_key", } attr_cols = [c for c in physical_names if c not in ignored and c != "id"] - wide_names = [] - for c in CORE_COLUMNS: - if c in ("player/id", "team/id", "entity_key"): - continue - phys_name = COLUMN_MAPPING.get(c, c) - if phys_name in physical_names: - wide_names.append(c) - - for row in entities: - p_id = str(row.get("entity_id")) - t_id = str(row.get("team_id", "none")) - entity = TrackedEntity.from_entity_key(f"{p_id}|{t_id}") - prefix = entity.cdf_prefix() - - for attr in attr_cols: - if attr != "entity_key": - wide_names.append(f"{prefix}{attr}") - for attr in row.keys(): - if attr not in ("entity_id", "team_id", "entity_key"): - wide_names.append(f"{prefix}{attr}") - - # Always add player/id (except for ball which has it via TrackedEntity logic if needed, - # but usually ball doesn't have player/id) - if not entity.is_ball: - wide_names.append(f"{prefix}player/id") + wide_names = [ + c + for c in CORE_COLUMNS + if c not in ("player/id", "team/id", "entity_key") + and COLUMN_MAPPING.get(c, c) in physical_names + ] - # Only add team/id for players and teams - if entity.is_player or entity.is_team: - wide_names.append(f"{prefix}team/id") + for entity, attrs in self._get_entities_from_metadata(entities_hier): + wide_names.extend(self._get_wide_names_for_entity(entity, attrs, attr_cols)) self._cached_schema = Fields(list(dict.fromkeys(wide_names))) return self._cached_schema def select(self, items: str | Sequence[str]) -> CatalogLazyTrackingDataContainer: """Translate wide selection to long selection pushdown.""" - assert self._catalog is not None return CatalogLazyTrackingDataContainer( self._catalog, self._match_id, @@ -636,7 +551,6 @@ def select(self, items: str | Sequence[str]) -> CatalogLazyTrackingDataContainer def slice(self, offset: int, length: int) -> CatalogLazyTrackingDataContainer: """Apply limit to the lazy container.""" - assert self._catalog is not None return CatalogLazyTrackingDataContainer( self._catalog, self._match_id, @@ -648,14 +562,11 @@ def slice(self, offset: int, length: int) -> CatalogLazyTrackingDataContainer: def collect(self) -> TrackingDataContainer: """Finally perform the Iceberg scan and pivot.""" - assert self._catalog is not None if self._catalog._tracking_table is None: self._catalog._tracking_table = self._catalog._iceberg_catalog.load_table( "tracking.tracking_data" ) - assert self._catalog._tracking_table is not None - filter_expr: BooleanExpression = EqualTo( term=Reference("match_id"), value=cast(Any, self._match_id) ) # type: ignore @@ -665,112 +576,85 @@ def collect(self) -> TrackingDataContainer: EqualTo(term=Reference("entity_id"), value=cast(Any, self._player_id)), # type: ignore ) + # 1. Column Pushdown Calculation selected_long_fields = None deferred_select = None - metadata = self._catalog.get_metadata(self._match_id) entities_hier = metadata.get("_entities", {}) - entities_flat = self._get_flat_entities(entities_hier) + parsed_entities = self._get_entities_from_metadata(entities_hier) if self._selection: eager_schema = self.schema() - literal_names: list[str] = [] - input_items = ( - self._selection if isinstance(self._selection, (list, tuple)) else [self._selection] + literal_names = ( + eager_schema.match(self._selection) + if isinstance(self._selection, str) and "*" in self._selection + else ( + [self._selection] if isinstance(self._selection, str) else list(self._selection) + ) ) - for item in input_items: - if isinstance(item, str): - if "*" in item: - literal_names.extend(eager_schema.match(item)) - else: - literal_names.append(item) - else: - literal_names.append(str(item)) - - literal_names = list(dict.fromkeys(literal_names)) - deferred_select = literal_names - - needed_long_internal = set(CORE_COLUMNS) - needed_ents: set[str] = set() - - for name in literal_names: - if name in CORE_COLUMNS: - needed_long_internal.add(name) - else: - field = DataField(name) - needed_long_internal.add(field.attribute) - if field.entity: - needed_ents.add(cast(str, field.entity_key())) + deferred_select = list(dict.fromkeys(literal_names)) + needed_long_internal = { + DataField(name).attribute if name not in CORE_COLUMNS else name + for name in deferred_select + } | set(CORE_COLUMNS) physical_names = {f.name for f in self._catalog._tracking_table.schema().fields} - selected_long_fields_set = set() - for f in needed_long_internal: - phys_name = COLUMN_MAPPING.get(f, f) - if phys_name in physical_names: - selected_long_fields_set.add(phys_name) + selected_long_fields = [ + COLUMN_MAPPING.get(f, f) + for f in needed_long_internal + if COLUMN_MAPPING.get(f, f) in physical_names + ] for req in [ "match_id", "entity_type", "entity_id", - "team_id", "frame_id", "timestamp", "period", "x", "y", ]: - if req in physical_names: - selected_long_fields_set.add(req) - - selected_long_fields = list(selected_long_fields_set) - - if needed_ents: - entity_predicate: BooleanExpression | None = None - for k in needed_ents: - p_id, t_id = k.split("|") - cond: BooleanExpression = EqualTo( - term=Reference("entity_id"), value=cast(Any, p_id) - ) # type: ignore - if p_id != "ball" and t_id != "none": - cond = And(cond, EqualTo(term=Reference("team_id"), value=cast(Any, t_id))) # type: ignore - - if entity_predicate is None: - entity_predicate = cond - else: - entity_predicate = Or(entity_predicate, cond) - - if entity_predicate: - filter_expr = And(filter_expr, entity_predicate) + if req in physical_names and req not in selected_long_fields: + selected_long_fields.append(req) + # 2. Perform the scan scan_kwargs: dict[str, Any] = {"row_filter": filter_expr} - if selected_long_fields is not None: + if selected_long_fields: scan_kwargs["selected_fields"] = tuple(selected_long_fields) if self._limit: scan_kwargs["limit"] = self._limit * 50 - scan = self._catalog._tracking_table.scan(**scan_kwargs) - arrow_table = scan.to_arrow() + arrow_table = self._catalog._tracking_table.scan(**scan_kwargs).to_arrow() + + # 3. Pivot using Rust core + mapping: dict[str, str] = {} + for entity, _ in parsed_entities: + eid = ( + str(entity.player_id) + if entity.is_player + else ( + f"referee:{entity.referee_id}" + if entity.is_referee + else (f"team:{entity.team_id}" if entity.is_team else "ball") + ) + ) + mapping[eid] = str(entity.team_id) if entity.team_id else "none" + + wide_container = TrackingDataContainer.pivot(arrow_table, entity_to_team=mapping) - wide_container = TrackingDataContainer.pivot(arrow_table) wide_container._catalog = self._catalog - # Inject constant attributes - if entities_flat: - wide_names = wide_container.schema().names() - for ent_data in entities_flat: - p_id = ent_data["entity_id"] - t_id = ent_data["team_id"] - prefix = TrackedEntity.from_entity_key(f"{p_id}|{t_id}").cdf_prefix() - for attr, value in ent_data.items(): - if attr in ("entity_id", "team_id", "entity_key"): - continue - - wide_attr_name = f"{prefix}{attr}" - if wide_attr_name not in wide_names: - wide_container = wide_container.add_column( - wide_attr_name, [value] * wide_container.num_rows() - ) + # 4. Inject constant attributes from metadata + wide_names = wide_container.schema().names() + for entity, attrs in parsed_entities: + prefix = entity.cdf_prefix() + for attr, value in attrs.items(): + wide_name = f"{prefix}{attr}" + if wide_name not in wide_names: + wide_container = wide_container.add_column( + wide_name, [value] * wide_container.num_rows() + ) if deferred_select: wide_container = wide_container.select(deferred_select) diff --git a/python/tracking_data_container/container.py b/python/tracking_data_container/container.py index d7b1732..1a6f4d1 100644 --- a/python/tracking_data_container/container.py +++ b/python/tracking_data_container/container.py @@ -356,7 +356,9 @@ def from_arrow( @classmethod def pivot( - cls, df: pl.DataFrame | pa.Table | pa.StructArray | pa.RecordBatch + cls, + df: pl.DataFrame | pa.Table | pa.StructArray | pa.RecordBatch, + entity_to_team: dict[str, str] | None = None, ) -> TrackingDataContainer: """Pivots long-format tracking data into wide-format using Rust implementation.""" if hasattr(df, "to_arrow"): @@ -373,7 +375,7 @@ def pivot( df = pa.RecordBatch.from_arrays(df.flatten(), names=[f.name for f in df.type]) # Use the underlying Rust implementation for performance - inner = _TrackingDataContainer.pivot(df) + inner = _TrackingDataContainer.pivot(df, entity_to_team) return TrackingDataContainer(inner, validate=False) def lazy(self) -> LazyTrackingDataContainer: diff --git a/python/tracking_data_container/schema.py b/python/tracking_data_container/schema.py index 307a2fb..01351d9 100644 --- a/python/tracking_data_container/schema.py +++ b/python/tracking_data_container/schema.py @@ -103,7 +103,6 @@ def from_entity_key(key: str) -> TrackedEntity: if p_id == "ball": return Ball() if p_id.startswith("team:"): - # Use provided t_id if available, otherwise extract from p_id team_id = t_id if t_id != "none" else p_id[5:] return Team(team_id) if p_id.startswith("referee:"): @@ -112,8 +111,7 @@ def from_entity_key(key: str) -> TrackedEntity: if t_id != "none": return Player(t_id, p_id) - # We don't have a direct "OtherPlayer" class, so we use a dummy name to - # construct a DataField and get its entity + # Handle players without a team return TrackedEntity._from_inner(_DataField.from_name(f"other/{p_id}/x").entity()) @staticmethod From 6e0060fd96e9372d5786da836bcb0e3ec4fac8bb Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Tue, 21 Apr 2026 11:42:28 +0200 Subject: [PATCH 20/21] iceberg support --- RFC.md | 355 +- crates/tdc-core/src/datacontainer/builder.rs | 4 +- crates/tdc-core/src/datacontainer/core.rs | 6 +- crates/tdc-core/src/datacontainer/lazy.rs | 155 +- crates/tdc-core/src/schema/field.rs | 17 + crates/tdc-parsers/src/sportec.rs | 12 +- crates/tdc-python/src/container.rs | 175 +- crates/tdc-python/src/lib.rs | 4 + crates/tdc-python/src/schema.rs | 11 + crates/tdc-python/src/selector.rs | 13 + examples/01-basics.ipynb | 18 +- examples/02-parsing.ipynb | 108 +- examples/03-interop.ipynb | 2 +- examples/04-udfs.ipynb | 17 +- examples/05-lazy.ipynb | 2 +- examples/06-skeletal.ipynb | 54 +- examples/07-catalog.ipynb | 72 +- examples/08-idsse-warehouse.ipynb | 202 +- examples/20260423-presentation-liss/build.sh | 11 + .../20260423-presentation-liss/custom.css | 51 + .../img/building_bridges.jpeg | Bin 0 -> 371179 bytes .../img/coordinatefc.png | Bin 0 -> 58876 bytes .../img/pysport_logo.png | Bin 0 -> 36396 bytes .../presentation.ipynb | 443 + .../presentation.slides.html | 7740 +++++++++++++++++ examples/idsse_warehouse/catalog.db | Bin 20480 -> 0 bytes ...-96a2-48d6-8305-1e7155f857e6.metadata.json | 1 - ...-4270-4bd2-86dc-2b88858c0833.metadata.json | 1 - ...-c362-4a0f-a942-2fc8d6c51c96.metadata.json | 1 - ...-b298-4f4f-a814-9b9f76d8e71a.metadata.json | 1 - ...c097fb-ed6a-4e53-95aa-61c36e19e9b5-m0.avro | Bin 4505 -> 0 bytes ...8d49d5-48bd-40a1-aac5-ac3df2728952-m0.avro | Bin 4508 -> 0 bytes ...b6cbfa-fef9-4779-b2e1-c63290db081c-m0.avro | Bin 4507 -> 0 bytes ...-eab6cbfa-fef9-4779-b2e1-c63290db081c.avro | Bin 1947 -> 0 bytes ...-13c097fb-ed6a-4e53-95aa-61c36e19e9b5.avro | Bin 1902 -> 0 bytes ...-748d49d5-48bd-40a1-aac5-ac3df2728952.avro | Bin 1827 -> 0 bytes ...-0ccc-4e61-aa83-a184ad41f6f1.metadata.json | 1 - ...-f126-4491-bfa6-00cf45343e37.metadata.json | 1 - ...-ab81-46c9-92b2-74595885722c.metadata.json | 1 - ...-78aa-49d0-8fa1-f6314d7b0ee9.metadata.json | 1 - ...6c935e-8816-455f-987e-f46e735afb70-m0.avro | Bin 5590 -> 0 bytes ...a2fe75-8006-42c8-97d9-5e4835aa023e-m0.avro | Bin 6126 -> 0 bytes ...eff7a4-107d-40c3-846c-6d69ba0f72ff-m0.avro | Bin 6110 -> 0 bytes ...-3aa2fe75-8006-42c8-97d9-5e4835aa023e.avro | Bin 1851 -> 0 bytes ...-236c935e-8816-455f-987e-f46e735afb70.avro | Bin 2001 -> 0 bytes ...-b2eff7a4-107d-40c3-846c-6d69ba0f72ff.avro | Bin 1936 -> 0 bytes python/tests/conftest.py | 21 + python/tests/test_add_column.py | 69 - python/tests/test_advanced_features.py | 102 - python/tests/test_catalog.py | 278 +- python/tests/test_container.py | 169 +- python/tests/test_lazy.py | 160 + python/tracking_data_container/__init__.py | 8 +- .../_tracking_data_container.pyi | 1 + .../backends/__init__.py | 5 + .../tracking_data_container/backends/base.py | 45 + .../backends/datafusion.py | 154 + .../backends/iceberg.py | 305 + python/tracking_data_container/catalog.py | 664 -- .../catalogs/__init__.py | 4 + .../tracking_data_container/catalogs/base.py | 83 + .../catalogs/iceberg.py | 439 + python/tracking_data_container/config.py | 24 - python/tracking_data_container/container.py | 614 +- python/tracking_data_container/io.py | 4 +- python/tracking_data_container/schema.py | 19 +- 66 files changed, 11066 insertions(+), 1582 deletions(-) create mode 100755 examples/20260423-presentation-liss/build.sh create mode 100644 examples/20260423-presentation-liss/custom.css create mode 100644 examples/20260423-presentation-liss/img/building_bridges.jpeg create mode 100644 examples/20260423-presentation-liss/img/coordinatefc.png create mode 100644 examples/20260423-presentation-liss/img/pysport_logo.png create mode 100644 examples/20260423-presentation-liss/presentation.ipynb create mode 100644 examples/20260423-presentation-liss/presentation.slides.html delete mode 100644 examples/idsse_warehouse/catalog.db delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00000-f1bd0a1f-96a2-48d6-8305-1e7155f857e6.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00001-ee062ad7-4270-4bd2-86dc-2b88858c0833.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00002-9c72b796-c362-4a0f-a942-2fc8d6c51c96.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/00003-b3d28f12-b298-4f4f-a814-9b9f76d8e71a.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/13c097fb-ed6a-4e53-95aa-61c36e19e9b5-m0.avro delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/748d49d5-48bd-40a1-aac5-ac3df2728952-m0.avro delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/eab6cbfa-fef9-4779-b2e1-c63290db081c-m0.avro delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/snap-2148665455330551612-0-eab6cbfa-fef9-4779-b2e1-c63290db081c.avro delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/snap-2774007322244479359-0-13c097fb-ed6a-4e53-95aa-61c36e19e9b5.avro delete mode 100644 examples/idsse_warehouse/tracking/match_metadata/metadata/snap-5536800293271387766-0-748d49d5-48bd-40a1-aac5-ac3df2728952.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00000-eb9e2be6-0ccc-4e61-aa83-a184ad41f6f1.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00001-bf153c68-f126-4491-bfa6-00cf45343e37.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00002-d345f9cd-ab81-46c9-92b2-74595885722c.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/00003-97d8ffab-78aa-49d0-8fa1-f6314d7b0ee9.metadata.json delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/236c935e-8816-455f-987e-f46e735afb70-m0.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/3aa2fe75-8006-42c8-97d9-5e4835aa023e-m0.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/b2eff7a4-107d-40c3-846c-6d69ba0f72ff-m0.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-3805096703135495509-0-3aa2fe75-8006-42c8-97d9-5e4835aa023e.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-6127150891889527239-0-236c935e-8816-455f-987e-f46e735afb70.avro delete mode 100644 examples/idsse_warehouse/tracking/tracking_data/metadata/snap-6133585602095476169-0-b2eff7a4-107d-40c3-846c-6d69ba0f72ff.avro delete mode 100644 python/tests/test_add_column.py delete mode 100644 python/tests/test_advanced_features.py create mode 100644 python/tests/test_lazy.py create mode 100644 python/tracking_data_container/backends/__init__.py create mode 100644 python/tracking_data_container/backends/base.py create mode 100644 python/tracking_data_container/backends/datafusion.py create mode 100644 python/tracking_data_container/backends/iceberg.py delete mode 100644 python/tracking_data_container/catalog.py create mode 100644 python/tracking_data_container/catalogs/__init__.py create mode 100644 python/tracking_data_container/catalogs/base.py create mode 100644 python/tracking_data_container/catalogs/iceberg.py delete mode 100644 python/tracking_data_container/config.py diff --git a/RFC.md b/RFC.md index 4cf272b..4b1559d 100644 --- a/RFC.md +++ b/RFC.md @@ -10,7 +10,7 @@ The container combines high-performance, zero-copy data access with a rich metad ## Motivation -As tracking pipelines grow across football and other sports, the need for modularity, performance, and inter-package interoperability becomes critical. A growing number of open-source packages in football analytics are duplicating functionality across parsing, modeling, and data management. This [comparison spreadsheet](https://docs.google.com/spreadsheets/d/1sfkqxkmtmQkXmkHiuNz-kvBvTLL7UWpCHdLuxW4zAF4/edit?gid=0#gid=0) illustrates overlapping efforts across different research groups and tools. +As tracking pipelines grow across football and other sports, the need for modularity, performance, and inter-package interoperability becomes critical. A growing number of open-source packages in football analytics are duplicating functionality across parsing, modeling, and data management. This [comparison spreadsheet](https://docs.google.com/spreadsheets/d/1sfkqxkmtmQkXmkHiuNz-kvBvTLL7UWpCHdLuxW4zAF4/edit?gid=0#gid=0) illustrates overlapping efforts across different research groups and tools. Current challenges: @@ -99,13 +99,13 @@ We propose to align column naming with the [Common Data Format (CDF)](https://do Example conventions based on CDF: -| Concept | Column Name | -|-----------|---------------------------------------------| -| Ball | `ball_x`, `ball_y`, `ball_z` | -| Player | `teams/players/_x`, `_y` | -| Skeleton | `teams/players//_x` | -| Metrics | `package/metric_name` | -| Time | `timestamp`, `frame_id`, `period` | +| Concept | Column Name | +| -------- | ------------------------------------ | +| Ball | `ball_x`, `ball_y`, `ball_z` | +| Player | `teams/players/_x`, `_y` | +| Skeleton | `teams/players//_x` | +| Metrics | `package/metric_name` | +| Time | `timestamp`, `frame_id`, `period` | These conventions follow: @@ -124,16 +124,334 @@ By adhering to CDF, the `TrackingDataContainer` will be interoperable with other - Defines optional logical "layers" (e.g. tracking, skeleton, predictions) - Use kloppy Metadata objects -### Storage & Computation Model +--- + +## Storage Layer Specification + +### Schema Design + +#### Semi-Long Format + +TrackingDataContainer uses a **semi-long format**: one row per entity (player/ball) per timestamp, with measurements as separate columns. + +```sql +CREATE TABLE tracking_data ( + -- Identifiers + match_id STRING, -- Partition key + timestamp TIMESTAMP(us), -- Microsecond precision + frame_id BIGINT, -- Optional frame counter + period_id TINYINT, -- 1, 2, etc. + + -- Entity identification + player_id STRING, -- CV ID or stable player ID (dictionary encoded) + team_id STRING, -- "home" / "away" / null (dictionary encoded) + + -- Measurements + x FLOAT, + y FLOAT, + z FLOAT -- Nullable (height) +) PARTITIONED BY (match_id, bucket(10, player_id)); +``` + +**Rationale:** + +- **Semi-long format**: Scales with variable player counts, handles ID instability (10K+ IDs from CV systems) +- **String IDs with dictionary encoding**: Efficient storage, flexible for unstable/non-numeric IDs +- **Separate x,y,z columns**: Enables SIMD vectorization for per-player operations +- **Bucketing**: Distributes high-cardinality player_id across fixed number of files + +### Storage Architecture + +#### Apache Iceberg Table Format + +**Primary storage**: Apache Iceberg tables with Parquet file format + +``` +warehouse/ +├── match_metadata/ # Separate metadata table +│ ├── metadata/ +│ └── data/ +└── tracking_data/ # Main tracking data table + ├── metadata/ + └── data/ + ├── match_id=123/ + │ ├── player_id_bucket=0/*.parquet + │ ├── player_id_bucket=1/*.parquet + │ └── ... + └── match_id=124/ + └── ... +``` + +**Why Iceberg:** + +- ✅ Stores optimization settings in metadata (bucket_count, sort_order) +- ✅ Schema evolution built-in +- ✅ Time-travel / versioning +- ✅ Can swap file format (Parquet → Lance/Vortex) via maintenance jobs +- ✅ Mature ecosystem (DuckDB, Spark, Trino support) + +**Catalog**: Start with filesystem catalog, design supports REST/Glue/Hive catalogs + +### Two-Table Design + +#### Table 1: match_metadata (Metadata Storage) + +```sql +CREATE TABLE match_metadata ( + match_id STRING PRIMARY KEY, + + -- Flexible JSON columns for Kloppy metadata + teams JSON, -- {home: {...}, away: {...}} + players JSON, -- [{id, name, jersey_number}, ...] + pitch JSON, -- {length, width, coordinate_system} + periods JSON, -- [{id, start, end}, ...] + + -- Optional metadata + competition_id STRING, + match_date DATE, + venue STRING, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +**Storage**: Non-partitioned table (small, ~1 row per match) + +#### Table 2: tracking_data (Tracking Data) -The container is designed to support **lazy evaluation** and **filter pushdown** using Apache Arrow-compatible formats like Parquet and Iceberg. +```sql +CREATE TABLE tracking_data ( + match_id STRING, + timestamp TIMESTAMP, + player_id STRING, + team_id STRING, + x FLOAT, y FLOAT, z FLOAT + -- Additional columns added via .add_column() +) PARTITIONED BY (match_id, bucket(10, player_id)); +``` + +**Storage**: Partitioned by match, bucketed by player + +### Partitioning Strategy + +#### Per-Match Use Case (Initial) + +```sql +PARTITIONED BY ( + match_id, -- One partition per match + bucket(10, player_id) -- 10 buckets per match +) +``` + +**Configurable bucket count:** + +```python +container.save('warehouse/tracking_data', bucket_count=10) +``` + +**Partition Layout:** + +``` +match_id=123/ + player_id_bucket=0/data.parquet (~10% of players, sorted by player_id) + player_id_bucket=1/data.parquet + ... + player_id_bucket=9/data.parquet +``` -- **Lazy Evaluation**: All transformations (e.g. metric calculations, filters) are applied lazily using Polars' `LazyFrame` or Arrow compute APIs. -- **Filter Pushdown**: When reading from Parquet or Iceberg, the container supports pushing column and row filters down to the storage engine, dramatically improving performance for large datasets. -- **Column Projection**: Only the necessary columns are loaded or materialized, based on selectors or expressions. -- **Iceberg Integration (Future Direction)**: Iceberg can serve as the underlying storage format, offering versioning, schema evolution, and partition pruning. The container will provide adapters to read from and write to Iceberg tables. +**Future use cases** (different partitioning schemes): + +- Competition-level: `PARTITIONED BY (competition_id, match_id, bucket(player_id))` +- Player-centric: `PARTITIONED BY (player_id, match_id)` + +### Optimization Settings + +#### Parquet File Layout + +**Within each partition file:** + +1. **Sorting**: `ORDER BY player_id ASC, timestamp ASC` + - Groups each player's data contiguously + - Enables page-level pruning + +2. **Row Groups**: 128 MB (default) + - Balance between parallelism and metadata overhead + +3. **Page Size**: 1 MB + - **Critical optimization**: Aligns with per-player data size (~750 KB compressed) + - One player ≈ one page → perfect page-level isolation + - Page index enables skipping irrelevant players + +4. **Page Index**: Enabled + - Stores min/max per page + - Essential for sorted data performance + +5. **Compression**: Configurable (default: `snappy`) + - `snappy`: Fast compression/decompression + - `zstd`: Better ratio, slightly slower + +6. **Dictionary Encoding**: Enabled for `player_id`, `team_id` + - Efficient for repeated string values + +**Example configuration:** + +```python +container.save('warehouse/tracking_data', + bucket_count=10, + compression='snappy', + page_size=1024*1024, # 1 MB (advanced) +) +``` -This design ensures the container can be scaled from local processing to lakehouse-style infrastructure with minimal friction. +### Write Strategy + +#### Partition-Level Replacement + +When saving a match, the entire partition is replaced: + +```python +def save(self, path: str, match_id: str): + """Save container (replaces entire match partition).""" + + # 1. Upsert metadata (atomic at table level) + MERGE INTO match_metadata + USING new_metadata + ON match_metadata.match_id = new_metadata.match_id + WHEN MATCHED THEN UPDATE SET * + WHEN NOT MATCHED THEN INSERT *; + + # 2. Replace tracking data partition (atomic at partition level) + DELETE FROM tracking_data WHERE match_id = '123'; + INSERT INTO tracking_data SELECT * FROM new_data; +``` + +**Benefits:** + +- ✅ Idempotent (can re-run safely) +- ✅ Partition replacement is atomic in Iceberg (manifest update) +- ✅ Can rewrite entire match with corrections/new columns + +**Implementation:** DuckDB or PyIceberg (to be validated) + +### Query Performance + +#### Multi-Level Pruning + +Iceberg + optimized Parquet enables efficient queries: + +```python +# Query: Get player 7's data for match 123 +container = TrackingDataContainer.load( + 'warehouse/tracking_data', + match_id='123', + player_id='7' +) +``` + +**Execution plan:** + +1. **Partition pruning**: Only read `match_id=123` partition (skip all other matches) +2. **Bucket pruning**: Hash player_id → read only bucket 7 (skip 90% of files) +3. **Row group pruning**: Check row group stats, skip groups without player 7 +4. **Page-level pruning**: Check page index, read only pages containing player 7 +5. **Result**: ~1 MB read (out of 100+ MB total for match) + +**Pruning efficiency: ~99%** 🎯 + +### Backend Flexibility + +#### Design for Swappable File Formats + +```python +# Current (Parquet) +container.save('warehouse/tracking_data', backend='parquet') + +# Future (via Iceberg maintenance job) +# ALTER TABLE tracking_data SET TBLPROPERTIES ('write.format.default' = 'lance'); +container.save('warehouse/tracking_data', backend='lance') +``` + +**Iceberg allows changing file format without rewriting old data.** + +Potential future formats: + +- **Lance**: Optimized for random access, versioning +- **Vortex**: Columnar format with advanced compression +- **ORC**: Alternative columnar format + +**Skeletal data**: When needed, can store in separate table with different format optimized for high cardinality (22 players × 15 joints = 330 entities). + +### Known Limitations + +#### 1. Cross-Table Atomicity + +Metadata and tracking data are written to separate tables without transactional guarantees. If tracking data write fails after metadata is written, manual cleanup may be required. + +**Future improvements:** + +- Implement two-phase commit pattern +- Add application-level transaction log +- Consider storing metadata within tracking_data table for single-table atomicity + +#### 2. DuckDB Write Limitations + +DuckDB (as of v1.4) cannot UPDATE/DELETE partitioned Iceberg tables directly. Workarounds: + +- Use PyIceberg for writes +- Use DuckDB for reads (excellent query performance) +- Future: DuckDB may add partitioned write support + +### Storage API + +#### User-Facing API + +```python +# Save (TDC handles all optimization) +container.save('warehouse/tracking_data', match_id='123') + +# Load (TDC handles efficient querying) +container = TrackingDataContainer.load( + 'warehouse/tracking_data', + match_id='123', # Optional filters + player_id='7' +) + +# Advanced: Configure optimization +container.save('warehouse/tracking_data', + match_id='123', + bucket_count=50, # More granular partitioning + compression='zstd' # Better compression ratio +) +``` + +**Design principle**: Simple by default, configurable for advanced users. + +### References + +**Iceberg + DuckDB:** + +- [Writes in DuckDB-Iceberg – DuckDB](https://duckdb.org/2025/11/28/iceberg-writes-in-duckdb) +- [Apache Iceberg Writes with DuckDB](https://www.confessionsofadataguy.com/apache-iceberg-writes-with-duckdb-or-not/) +- [Iceberg Extension – DuckDB](https://duckdb.org/docs/stable/core_extensions/iceberg/overview) +- [Transaction Management – DuckDB](https://duckdb.org/docs/stable/sql/statements/transactions) + +**Iceberg Metadata & Partitioning:** + +- [Extending Apache Iceberg: Best Practices for Custom Metadata | Dremio](https://www.dremio.com/blog/extending-apache-iceberg-best-practices-for-storing-and-discovering-custom-metadata/) +- [Understanding Apache Iceberg's metadata.json File](https://dev.to/alexmercedcoder/understanding-apache-icebergs-metadatajson-file-23f) +- [Partitioning Apache Iceberg Tables | Medium](https://medium.com/@tglawless/partitioning-apache-iceberg-tables-ad2757cb0b76) +- [Iceberg Partitioning - Apache Iceberg](https://iceberg.apache.org/docs/latest/partitioning/) + +**Parquet Optimization:** + +- [Cloudera: Speeding Up Queries with Parquet Page Indexes](https://www.cloudera.com/blog/technical/speeding-up-select-queries-with-parquet-page-indexes.html) +- [DataFusion: Parquet Pruning](https://datafusion.apache.org/blog/2025/03/20/parquet-pruning/) +- [Parquet Performance Tuning – Medium](https://medium.com/data-engineering-with-dremio/all-about-parquet-part-10-performance-tuning-and-best-practices-with-parquet-d697ba4e8a57) + +**Cache Locality & Performance:** + +- [Spatial Locality - Algorithmica](https://en.algorithmica.org/hpc/external-memory/locality/) +- [Cache Locality Performance](https://arpitbhayani.me/blogs/cpu-cache-locality) +- [Polars Performance Blog](https://pola.rs/posts/i-wrote-one-of-the-fastest-dataframe-libraries/) --- @@ -150,14 +468,17 @@ This design ensures the container can be scaled from local processing to lakehou ## Rationale & Alternatives ### Why not just Arrow? + - Arrow is fast and standard, but lacks semantics - Metadata enables safe, interpretable, and sport-agnostic usage ### Why enforce the container? + - To guarantee consistency across packages - To avoid schema drift and partial implementations ### Why CDF and flat schema? + - CDF provides a well-vetted, community-driven naming standard - Flat schema increases compatibility and simplifies filter pushdown and projection - Structs may be supported later as an internal representation @@ -192,7 +513,8 @@ This design ensures the container can be scaled from local processing to lakehou - Related tickets: - [Refactor serializer options into own component](https://github.com/PySport/kloppy/issues/10) -> Describes a FrameBuilder approach - [Refactor tracking data model](https://github.com/PySport/kloppy/pull/377) -- Should TDC support transformations? Probably yes, so a package can ensure all data is in the correct orientation and dimensions. +- Should TDC support transformations? Probably yes, so a package can ensure all data is in the correct orientation and dimensions. + --- ## Conclusion @@ -206,6 +528,7 @@ We propose making this the default container for all tools in the ecosystem. We brought together contributors from different open source projects to discuss how we can align our work and improve interoperability in football analytics. Participants included: + - 🇧🇪 Pieter Robberechts (soccerdata, socceraction, Kloppy) - 🇳🇱 Alexander Oonk (databallpy) - 🇯🇵 Keisuke Fujii, Calvin Yeung (OpenSTARLab) diff --git a/crates/tdc-core/src/datacontainer/builder.rs b/crates/tdc-core/src/datacontainer/builder.rs index e4a087c..2c1bc12 100644 --- a/crates/tdc-core/src/datacontainer/builder.rs +++ b/crates/tdc-core/src/datacontainer/builder.rs @@ -1343,8 +1343,8 @@ impl TrackingDataBuilder { for ((name, mut builder), domain_field) in self .column_names .into_iter() - .zip(self.columns.into_iter()) - .zip(self.domain_fields.into_iter()) + .zip(self.columns) + .zip(self.domain_fields) { builder.ensure_capacity(total_rows); let (array, dtype) = builder.finish(); diff --git a/crates/tdc-core/src/datacontainer/core.rs b/crates/tdc-core/src/datacontainer/core.rs index d42c570..539c09a 100644 --- a/crates/tdc-core/src/datacontainer/core.rs +++ b/crates/tdc-core/src/datacontainer/core.rs @@ -90,7 +90,7 @@ impl TrackingDataContainer { .map(|(i, f): (usize, &Arc)| (f.name().clone(), i)) .collect(); - for (field, array) in fields.into_iter().zip(arrays.into_iter()) { + for (field, array) in fields.into_iter().zip(arrays) { let name = field.name(); if let Some(&i) = field_map.get(&name) { new_columns[i] = array.clone(); @@ -123,7 +123,7 @@ impl TrackingDataContainer { ) -> TrackingDataContainerResult { let mut df = ctx.read_batch(RecordBatch::from(self.data.clone()))?; let new_names = names.clone(); - for (name, expr) in names.into_iter().zip(exprs.into_iter()) { + for (name, expr) in names.into_iter().zip(exprs) { df = df.with_column(&name, expr)?; } df = df.select_columns(&new_names.iter().map(|s| s.as_str()).collect::>())?; @@ -747,7 +747,7 @@ impl crate::selector::Select for TrackingDataContainer { let mut new_arrow_fields = Vec::with_capacity(fields.len()); let mut new_schema_fields = Vec::with_capacity(fields.len()); - for (field, maybe_idx) in fields.into_iter().zip(indices.into_iter()) { + for (field, maybe_idx) in fields.into_iter().zip(indices) { if let Some(idx) = maybe_idx { new_arrays.push(self.data.column(idx).clone()); new_arrow_fields.push(self.data.fields()[idx].clone()); diff --git a/crates/tdc-core/src/datacontainer/lazy.rs b/crates/tdc-core/src/datacontainer/lazy.rs index 62889d5..f98f2bc 100644 --- a/crates/tdc-core/src/datacontainer/lazy.rs +++ b/crates/tdc-core/src/datacontainer/lazy.rs @@ -237,7 +237,7 @@ impl LazyTrackingDataContainer { let mut df = self.df.clone(); let mut new_schema_fields = self.schema.fields.clone(); - for (name, expr) in names.into_iter().zip(exprs.into_iter()) { + for (name, expr) in names.into_iter().zip(exprs) { df = df.with_column(&name, expr)?; let field = DataField::from_name(&name); let mut found = false; @@ -263,6 +263,159 @@ impl LazyTrackingDataContainer { }) } + pub fn melt(&self) -> TrackingDataContainerResult { + use crate::schema::TrackedEntity; + use crate::schema::attribute::Attribute; + use datafusion::logical_expr::ExprSchemable; + use std::collections::{HashMap, HashSet}; + + let core_names: HashSet<&str> = [ + "match/id", + "competition/id", + "season/id", + "timestamp", + "frame_id", + "period", + "period_id", + "ball_status", + "ball_poss_team_id", + "ball_poss_status", + ] + .iter() + .cloned() + .collect(); + + let mut core_cols = Vec::new(); + for f in &self.schema.fields { + let name = f.name(); + if core_names.contains(name.as_str()) { + core_cols.push(name); + } + } + + let mut entity_groups: HashMap> = HashMap::new(); + let mut all_attributes: HashSet = HashSet::new(); + let mut attr_datatypes: HashMap = HashMap::new(); + + let df_schema = self.df.schema(); + + for f in &self.schema.fields { + if let DataField::TrackedEntity { entity, attribute } = f { + let attr_name = attribute.name().to_string(); + if attr_name == "player/id" || attr_name == "team/id" || attr_name == "entity_key" { + continue; + } + all_attributes.insert(attr_name.clone()); + entity_groups + .entry(entity.clone()) + .or_default() + .insert(attr_name.clone(), f.name()); + + if !attr_datatypes.contains_key(&attr_name) + && let Ok(arrow_field) = df_schema.field_with_unqualified_name(&f.name()) + { + let mut dt = arrow_field.data_type().clone(); + if attr_name == "x" || attr_name == "y" || attr_name == "z" { + dt = arrow_schema::DataType::Float32; + } + attr_datatypes.insert(attr_name, dt); + } + } + } + + let mut all_attributes_vec: Vec = all_attributes.into_iter().collect(); + all_attributes_vec.sort(); + + let mut melted_dfs = Vec::new(); + + for (entity, attrs) in entity_groups { + let mut exprs = Vec::new(); + for c in &core_cols { + exprs.push(col(c)); + } + + let etype = match &entity { + TrackedEntity::Ball => "ball", + TrackedEntity::Player { .. } => "player", + TrackedEntity::Referee { .. } => "referee", + TrackedEntity::Team { .. } => "team", + }; + + let ekey = match &entity { + TrackedEntity::Ball => "ball".to_string(), + TrackedEntity::Player { team_id, player_id } => { + format!("{}|{}", player_id, team_id) + } + TrackedEntity::Referee { referee_id } => format!("referee:{}", referee_id), + TrackedEntity::Team { team_id } => format!("team:{}", team_id), + }; + + let parts: Vec<&str> = ekey.split('|').collect(); + let p_id = parts[0].to_string(); + let t_id = if parts.len() > 1 { + parts[1].to_string() + } else { + "none".to_string() + }; + + exprs.push(lit(p_id).alias("player/id")); + exprs.push( + lit(if t_id == "none" { + datafusion::scalar::ScalarValue::Utf8(None) + } else { + datafusion::scalar::ScalarValue::Utf8(Some(t_id)) + }) + .alias("team/id"), + ); + exprs.push(lit(ekey).alias("entity_key")); + exprs.push(lit(etype).alias("entity_type")); + + let mut filter_exprs = Vec::new(); + + for attr in &all_attributes_vec { + if let Some(col_name) = attrs.get(attr) { + let mut e = col(col_name); + if attr == "x" || attr == "y" || attr == "z" { + e = e.cast_to(&arrow_schema::DataType::Float32, df_schema)?; + } + exprs.push(e.alias(attr)); + filter_exprs.push(col(col_name).is_not_null()); + } else { + let dt = attr_datatypes + .get(attr) + .cloned() + .unwrap_or(arrow_schema::DataType::Float32); + exprs.push( + datafusion::logical_expr::Expr::Cast(datafusion::logical_expr::Cast { + expr: Box::new(lit(datafusion::scalar::ScalarValue::Null)), + data_type: dt, + }) + .alias(attr), + ); + } + } + + let filter_expr = filter_exprs.into_iter().reduce(|a, b| a.or(b)); + let mut entity_df = self.df.clone(); + if let Some(f) = filter_expr { + entity_df = entity_df.filter(f)?; + } + entity_df = entity_df.select(exprs)?; + melted_dfs.push(entity_df); + } + + if melted_dfs.is_empty() { + return Ok(self.df.clone().limit(0, Some(0))?); + } + + let mut final_df = melted_dfs[0].clone(); + for df in melted_dfs.into_iter().skip(1) { + final_df = final_df.union(df)?; + } + + Ok(final_df) + } + pub fn add_array_column( &self, _name: &str, diff --git a/crates/tdc-core/src/schema/field.rs b/crates/tdc-core/src/schema/field.rs index 798715a..ab89095 100644 --- a/crates/tdc-core/src/schema/field.rs +++ b/crates/tdc-core/src/schema/field.rs @@ -5,6 +5,23 @@ use arrow_schema::DataType; use std::fmt; use std::str::FromStr; +/// Standard core columns that identify the dataset, match, or frame context. +pub const CORE_COLUMNS: &[&str] = &[ + "match/id", + "competition/id", + "season/id", + "timestamp", + "frame_id", + "period", + "period_id", + "player/id", + "team/id", + "entity_key", + "ball_status", + "ball_poss_team_id", + "ball_poss_status", +]; + /// Represents a single column (field) in a tracking dataset. /// /// A `DataField` maps an Arrow column name to its domain-specific meaning, diff --git a/crates/tdc-parsers/src/sportec.rs b/crates/tdc-parsers/src/sportec.rs index 85131b0..dd63aec 100644 --- a/crates/tdc-parsers/src/sportec.rs +++ b/crates/tdc-parsers/src/sportec.rs @@ -144,15 +144,11 @@ pub fn load_sportec(path: &str, meta_path: Option<&str>) -> Result n = val.parse().unwrap_or(-1), - b"T" => { - if !val.is_empty() { - ts = Some(val.to_string()); - } + b"T" if !val.is_empty() => { + ts = Some(val.to_string()); } - b"M" => { - if !val.is_empty() { - p = Some(val.parse::().unwrap_or(0)); - } + b"M" if !val.is_empty() => { + p = Some(val.parse::().unwrap_or(0)); } _ => (), } diff --git a/crates/tdc-python/src/container.rs b/crates/tdc-python/src/container.rs index 7e6c338..a3085f0 100644 --- a/crates/tdc-python/src/container.rs +++ b/crates/tdc-python/src/container.rs @@ -223,7 +223,7 @@ impl PyTrackingDataContainer { let mut expr_values = Vec::new(); let mut array_names = Vec::new(); let mut array_values = Vec::new(); - for (i, (name, value)) in names.into_iter().zip(values.into_iter()).enumerate() { + for (i, (name, value)) in names.into_iter().zip(values).enumerate() { match value { AddColumnValue::Expr(e) => { expr_names.push(name); @@ -536,7 +536,7 @@ impl PyLazyTrackingDataContainer { let mut expr_names = Vec::new(); let mut expr_values = Vec::new(); - for (name, value) in names.into_iter().zip(values.into_iter()) { + for (name, value) in names.into_iter().zip(values) { match value { AddColumnValue::Expr(e) => { expr_names.push(name); @@ -631,6 +631,164 @@ impl PyLazyTrackingDataContainer { .call_method0(py, "__arrow_c_stream__")? .into_bound(py)) } + + fn melt(&self) -> PyResult { + Ok(PyLazyDataFrame { + df: self.inner.melt().into_pyresult()?, + }) + } +} + +#[pyclass(name = "LazyDataFrame", module = "tracking_data_container")] +pub struct PyLazyDataFrame { + df: datafusion::dataframe::DataFrame, +} + +#[pymethods] +impl PyLazyDataFrame { + fn __arrow_c_stream__<'py>( + &self, + py: Python<'py>, + _requested_schema: Option>, + ) -> PyResult> { + let stream = py + .allow_threads(|| block_on(async { self.df.clone().execute_stream().await })) + .map_err(|e| PyValueError::new_err(e.to_string()))?; + let schema = stream.schema(); + struct StreamReader { + stream: std::pin::Pin>, + schema: arrow_schema::SchemaRef, + } + impl Iterator for StreamReader { + type Item = Result; + fn next(&mut self) -> Option { + let next_batch = block_on(async { self.stream.next().await }); + next_batch.map(|res| { + res.map_err(|e| arrow_schema::ArrowError::ExternalError(Box::new(e))) + }) + } + } + impl datafusion::arrow::record_batch::RecordBatchReader for StreamReader { + fn schema(&self) -> arrow_schema::SchemaRef { + self.schema.clone() + } + } + let pyarrow_reader = + pyo3_arrow::PyRecordBatchReader::new(Box::new(StreamReader { stream, schema })) + .to_pyarrow(py)?; + Ok(pyarrow_reader + .call_method0(py, "__arrow_c_stream__")? + .into_bound(py)) + } + + fn extract_iceberg_metadata<'py>( + &self, + py: Python<'py>, + match_id: &str, + potential_constants: Vec, + ) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> { + use datafusion::prelude::*; + use pyo3::IntoPyObjectExt; + + // 1. Rename columns according to mapping + // COLUMN_MAPPING = {"match/id": "match_id", "competition/id": "competition_id", ...} + let df = self.df.clone(); + let mut select_exprs = Vec::new(); + for field in df.schema().fields() { + let name = field.name(); + if name == "match/id" { + select_exprs.push(col(name).alias("match_id")); + } else if name == "competition/id" { + select_exprs.push(col(name).alias("competition_id")); + } else if name == "season/id" { + select_exprs.push(col(name).alias("season_id")); + } else if name == "period_id" { + select_exprs.push(col(name).alias("period")); + } else if name == "player/id" { + select_exprs.push(col(name).alias("entity_id")); + } else { + select_exprs.push(col(name)); + } + } + + let df = py + .allow_threads(|| block_on(async { df.select(select_exprs) })) + .map_err(|e| PyValueError::new_err(e.to_string()))?; + + // 2. Add match_id column if not exists (we overwrite it for iceberg) + let df = py + .allow_threads(|| block_on(async { df.with_column("match_id", lit(match_id)) })) + .map_err(|e| PyValueError::new_err(e.to_string()))?; + + // 3. Aggregate constants + let group_exprs = vec![col("entity_id"), col("team/id"), col("entity_type")]; + let mut agg_exprs = Vec::new(); + for c in &potential_constants { + agg_exprs.push( + datafusion::functions_aggregate::expr_fn::count_distinct(col(c)) + .alias(format!("{}_count", c)), + ); + agg_exprs.push( + datafusion::functions_aggregate::expr_fn::first_value(col(c), None) + .alias(format!("{}_val", c)), + ); + } + + let agg_df = py + .allow_threads(|| block_on(async { df.clone().aggregate(group_exprs, agg_exprs) })) + .map_err(|e| PyValueError::new_err(e.to_string()))?; + + // 4. Collect agg_df to get the constants + let batches = py + .allow_threads(|| block_on(async { agg_df.collect().await })) + .map_err(|e| PyValueError::new_err(e.to_string()))?; + + // We will return the original (renamed, match_id added) PyLazyDataFrame, + // and the Arrow table of the aggregated stats. + // Then Python can figure out which columns are constant and drop them. + + // Convert batches to PyArrow Table + let schema = batches + .first() + .map(|b| b.schema()) + .unwrap_or_else(|| Arc::new(arrow_schema::Schema::empty())); + let mut arrow_batches = Vec::new(); + for b in batches { + arrow_batches.push(b); + } + + // Export aggregated table + let pyarrow = py.import_bound("pyarrow")?; + let table_class = pyarrow.getattr("Table")?; + let pa_table = pyo3_arrow::PyRecordBatchReader::new(Box::new( + datafusion::arrow::record_batch::RecordBatchIterator::new( + arrow_batches.into_iter().map(Ok), + schema, + ), + )) + .to_pyarrow(py)?; + let agg_table = table_class.call_method1("from_batches", (pa_table,))?; + + // Return a new PyLazyDataFrame for the main df + let new_df = PyLazyDataFrame { df }; + Ok((new_df.into_bound_py_any(py)?, agg_table)) + } + + fn drop_columns<'py>(&self, py: Python<'py>, cols: Vec) -> PyResult> { + let mut select_exprs = Vec::new(); + for field in self.df.schema().fields() { + let name = field.name(); + if !cols.contains(name) { + select_exprs.push(datafusion::prelude::col(name)); + } + } + let df = py + .allow_threads(|| block_on(async { self.df.clone().select(select_exprs) })) + .map_err(|e| PyValueError::new_err(e.to_string()))?; + let new_df = PyLazyDataFrame { df }; + use pyo3::IntoPyObjectExt; + new_df.into_bound_py_any(py) + } } impl PyLazyTrackingDataContainer { @@ -689,7 +847,7 @@ fn build_arrow_array(list: &Bound<'_, PyList>, dtype: &DataType) -> PyResult()?); + b.append_value(i.extract::()? as f32); } } Ok(Arc::new(b.finish())) @@ -743,8 +901,15 @@ fn build_arrow_array(list: &Bound<'_, PyList>, dtype: &DataType) -> PyResult() { + b.append_value(ts); + } else if let Ok(ts_val) = i.call_method0("timestamp") { + let ts = (ts_val.extract::()? * 1_000_000.0) as i64; + b.append_value(ts); } else { - b.append_value(i.extract::()?); + return Err(PyTypeError::new_err( + "Expected integer or datetime with timestamp method", + )); } } Ok(Arc::new(b.finish())) @@ -755,7 +920,7 @@ fn build_arrow_array(list: &Bound<'_, PyList>, dtype: &DataType) -> PyResult()?); + b.append_value(i.to_string()); } } Ok(Arc::new(b.finish())) diff --git a/crates/tdc-python/src/lib.rs b/crates/tdc-python/src/lib.rs index 2ab7ca8..ecc4158 100644 --- a/crates/tdc-python/src/lib.rs +++ b/crates/tdc-python/src/lib.rs @@ -26,6 +26,10 @@ fn _tracking_data_container(m: &Bound<'_, PyModule>) -> PyResult<()> { ops::register(m)?; m.add("__version__", env!("CARGO_PKG_VERSION"))?; + m.add( + "CORE_COLUMNS", + tdc_core::schema::field::CORE_COLUMNS.to_vec(), + )?; Ok(()) } diff --git a/crates/tdc-python/src/schema.rs b/crates/tdc-python/src/schema.rs index 84fd618..0d2d07f 100644 --- a/crates/tdc-python/src/schema.rs +++ b/crates/tdc-python/src/schema.rs @@ -41,6 +41,17 @@ impl PyDataField { DataField::TrackedEntity { attribute, .. } => attribute.name().to_string(), } } + + fn to_iceberg_name(&self) -> String { + match self.inner.name().as_str() { + "match/id" => "match_id".to_string(), + "competition/id" => "competition_id".to_string(), + "season/id" => "season_id".to_string(), + "player/id" => "entity_id".to_string(), + "team/id" => "team_id".to_string(), + other => other.to_string(), + } + } fn entity(&self) -> Option { match &self.inner { DataField::Frame { .. } => None, diff --git a/crates/tdc-python/src/selector.rs b/crates/tdc-python/src/selector.rs index 42cc6bb..8912362 100644 --- a/crates/tdc-python/src/selector.rs +++ b/crates/tdc-python/src/selector.rs @@ -37,6 +37,11 @@ pub struct PyColumnSelector; #[pymethods] impl PyColumnSelector { + #[new] + fn new() -> Self { + PyColumnSelector + } + fn ball(&self) -> PySelection { PySelection { expr: CoreExpr::Ball, @@ -69,6 +74,14 @@ pub struct PyExpr { #[pymethods] impl PyExpr { + fn __repr__(&self) -> String { + format!("{:?}", self.inner) + } + + fn __str__(&self) -> String { + format!("{}", self.inner) + } + fn __add__(&self, other: PyExpr) -> PyExpr { PyExpr { inner: self.inner.clone() + other.inner, diff --git a/examples/01-basics.ipynb b/examples/01-basics.ipynb index 93f9b5a..7ecc21c 100644 --- a/examples/01-basics.ipynb +++ b/examples/01-basics.ipynb @@ -2,7 +2,13 @@ "cells": [ { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "source": [ "# Basics: TrackingDataContainer and CDF Schema\n", "\n", @@ -53,7 +59,13 @@ { "cell_type": "code", "execution_count": 1, - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "outputs": [ { "name": "stdout", @@ -89,7 +101,7 @@ " \"teams/away/players/p2/y\": [34.9, 35.0, 35.1],\n", "}\n", "\n", - "container = tdc.TrackingDataContainer(data)\n", + "container = tdc.TrackingDataContainer(data, validate=False)\n", "print(f\"Container created with {container.num_rows()} frames.\")" ] }, diff --git a/examples/02-parsing.ipynb b/examples/02-parsing.ipynb index f1e0962..c6e0e7e 100644 --- a/examples/02-parsing.ipynb +++ b/examples/02-parsing.ipynb @@ -2,7 +2,13 @@ "cells": [ { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "source": [ "# Efficient Parsing with TrackingDataBuilder\n", "\n", @@ -29,7 +35,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Ingested 10000 frames in 0.0715 seconds.\n" + "Ingested 10000 frames in 0.0768 seconds.\n" ] } ], @@ -152,9 +158,9 @@ "output_type": "stream", "text": [ "--- Loading benchmark ---\n", - "TDC (Rust): 1.18s\n", - "Fast-Forward: 1.60s\n", - "Kloppy: 19.38s\n" + "TDC (Rust): 1.41s\n", + "Fast-Forward: 1.69s\n", + "Kloppy: 20.04s\n" ] } ], @@ -185,6 +191,48 @@ "print(f\"Kloppy: {kloppy_load_time:.2f}s\")" ] }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA9sAAAGGCAYAAABrBpLcAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAXC5JREFUeJzt3Qd4FOXaxvE3hRKBhN6RJiKiYkfsisfeK9grKjbs7Shy7L0XbGBXVOweC1ZUVEAERFCaCAhSlN6T+a77Pc5+m2STTcIk+2z4/65rCdk6O/fuZp9533kmIwiCwAEAAAAAgMhkRndXAAAAAACAYhsAAAAAgErAyDYAAAAAABGj2AYAAAAAIGIU2wAAAAAARIxiGwAAAACAiFFsAwAAAAAQMYptAAAAAAAiRrENAAAAAEDEKLYBYAPw+eefu4yMDPfaa6+5dHDDDTf45V2wYEGqFwXwfvvtN/+aHDx4cLHXaXW3bNkyd+aZZ7rmzZv759uvX79ULxIApAWKbQBYT/ryrS+g8aemTZu6vfbay/33v/9l/RryzjvvuD322MPns9FGG7kOHTq4Y4891n3wwQcpW6YXX3zR3XfffVX+uEEQuN122801adLELVy4sNjl55xzjqtRo4b78ccfCxWbOt10000J7/OEE07wl9etW7fQ+XvuuafbYostqv1Goqp06qmnFvrMyc3Ndd26dXN33323W716daSPdcstt/jPuXPPPdc999xz7qSTTor0/gGguqLYBoCI/Oc///FfRJ999ll3xRVXuPnz57sDDzzQvfvuu6xjA+666y536KGH+sLk6quvdvfee6876qij3OTJk93LL7+8wRXbWg8DBw50ixcvdpdddlmhy0aMGOEef/xxd9FFF7mtt9660GW1a9d2L730UrH7W758uXvrrbf85RuKf//7327lypUpe/xatWr5zxydVBA3bNjQZ3nKKadE+jiffvqp22mnnVz//v3diSee6LbbbrtI7x8AqqvsVC8AAFQXBxxwgNt+++1jv59xxhmuWbNmvjA5+OCDU7ps6UIFW506dSK/33Xr1rkbb7zR/etf/3IfffRRscvnzZvnqstzLY/NN9/cXX755b5Q00ipRv3Xrl3r+vTp49q0aeMGDBhQ7DbagDR06FA3duxYP5IaUqG9Zs0at//++/vibEOQnZ3tT6l8fBW/ob59+7ru3bu7V155xd1zzz2uZcuWFb7vgoICn6c2nuj9oddKlO9H3X/NmjUju08AsIiRbQCoJPXr13c5OTnFvozrS6ZGMrt27eq/yKogP/vss93ff/9d6Hrt2rXzRfpXX33ldtxxR39dTXvWyHlRixYtchdffLG/jUa7Wrdu7U4++eRi+zzrsW+++WZ/ue6vZ8+ebsqUKQmn/I4bN84XX5puvckmm8Sm8n7xxRf+C72eW+fOnd2wYcMK3X7GjBn+S78u03UaNWrkjjnmGD8NOdH0e92frq+p3Vqukuh+tRxatj///NOft2LFCjdp0qSk+3br8iVLlrhddtkl4eV67KJTl1WwXHPNNX4/VRXFGhWfOXNmsdu++uqrfqRPz7Vx48a++Jk9e3ah66iQ1dTqqVOn+mK1Xr16fsq11vV7773nn1s4HVgZhjQdWKOJet7KVQWwZk0kmib8/PPP+9eJ8mrQoIHbfffdE25YKOq6665zHTt29K9BFVeahvzTTz+5hx56KOHGgB49erj27dv7Efl4L7zwgi+0Nbpa1aZNm+ZfY3psPX+Nwmq9xtNzu/76631WeXl5/rlpGv1nn32W8P2kzHQ9vY81Uqzzikq0z7Z+P//8892bb77pX6vKTe/1RLsq6LWmDXR6LyoDzTRYn/3AMzMz/WtKwvdbWV9D4XIrRy2vrqtl1vnTp0/36zN8jYb3rSI83Kio56CNL88880yh+w13P9DMEn3u6Xnqvn/++efYc/3111/9+0brW7s16DWp3Rz0fjvssMP8FHm9D/XarEim8cugGRvhMuywww5u5MiRxdajPlO0e4mWJfycu/baawtdR+/x008/3T/3MOOnn366QrkBqL4Y2QaAiGg6roo6fUnUl9AHH3zQNxaKH3kSFTUqNE877TR34YUX+i+yKmzGjBnjvv76a7+fbEiF8NFHH+2/0OoLv77MqQjQl0t9uRM9hr5gTpw40X/523bbbf1yvP32227WrFm+AAzddttt/gu5pppqee+44w5f9H333XeFllGFvwr9Xr16+SLm0Ucf9f/XF3E1R9L+vMcff7y78847/fLpS7EKSNGX12+++cZfX8Wzvujq9ioC9AVbxVA8Fdr6UqsvzRrtTURF6t577+2LqY8//jj2nL7//nu/b7yKCX1xL4mKaX1p1j7bF1xwQZkKQm2U0Bf0K6+80uepQmGfffbx+zDrviTMUV/ab731Vr8R4P777/c5Kk8VavGjefvtt5/bdddd/Zd+rQcVEMpBOWlau4T7O2vDiAp8bWzRSHOXLl3c+PHj/fVUnKiYC2kEWs9/55139rszaMRQmWqEed999y31eapIeuSRR/yyKQsV0UcccYQ75JBDSrxN7969fXGv11PYyE6FvaYzV/X+71rnet7a8KL3kzbuqODTutMGIj0X0caWJ5980i/7WWed5ZYuXeqeeuop/7z1Ogqny+v9qwJP612vc633N954o1xTs3Vbjf5rfep98cADD/hdFn7//Xe/fKLXhzZOtGjRwueXn5/vs9N7YX3ovSJ6nPK8hkSvlyFDhviiW+8xLZsy1YY8vZcvvfRSfz0to6bP6z2tzyhdXxtgtOFJn0/aMKFdEOINGjTIrVq1yi+HitP49+Bxxx3nl02vJxX16gmgy7XxQe/722+/3X/26HNL7zVtSCpPpiG9tnUdfQbrdavPvyOPPNJvrAk/d7WRUZ+n+l3Lqo1fWqf67NBnQvia0wadcAOF1of6c+hzWstEAzkAMQEAYL0MGjQo0Mdp0VOtWrWCwYMHF7ru8OHD/WUvvPBCofM/+OCDYue3bdvWn/fll1/Gzps3b56/30svvTR23vXXX++vN3To0GLLVlBQ4H9+9tln/jpdunQJVq9eHbv8/vvv9+ePHz8+dt4ee+zhz3vxxRdj502aNMmfl5mZGXz77bex8z/88EN/vtZBaMWKFcWWY8SIEf56zz77bLH1tuuuuwbr1q0rdP3+/fv7y+bPnx9MnDgxaNmyZbDDDjsEf/31V6Hrhc9L108mXE916tQJDjjggODmm28ORo8eXex64X22atUqWLJkSez8IUOG+PO1zmTNmjVB06ZNgy222CJYuXJl7Hrvvvuuv54eL3TKKaf486666qpij3fQQQf5rIt67rnn/PrWaybeY4895u/r66+/9r9PnjzZX++II44I8vPzE+ZfFr179/b3W69evWDmzJnFLp8+fbq//M477wx++ukn//9w2R5++OGgbt26wfLly/1z1TqOp9dU165dg/IKs3j11VdLvE6/fv0KLYssXbo0aN++fdCuXbvYOtFrLP61L3///XfQrFmz4PTTT4+d9+abb/r7u+OOO2Ln6ba77bZbsdd6+DqNp99r1qwZTJkyJXbe2LFj/fkPPvhg7LxDDjkk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AAAAZbB06VKXl5fnqhLFdppp2LCh//n7779X+YsFJW8ta9OmjZs5c6bLzc1lNaUYedhDJvaQiT1kYgt52EMm9qRLJkEQ+EK7ZcuWVf7YFNtpenxtFdqWX9QbIuVBJnaQhz1kYg+Z2EMmtpCHPWRiTzpkkpeiQUoapAEAAAAAEDGKbQAAAAAAIkaxnWZq1arl+vfv73/CBjKxhTzsIRN7yMQeMrGFPOwhE3vIJLmMIBU90AEAAAAAqMYY2QYAAAAAIGIU2wAAAAAARIxiGwAAAACAiFFsG/Twww+7du3audq1a7vu3bu777//vtTrv/rqq26zzTbz199yyy3d+++/X2XLWt3deuutbocddnD16tVzTZs2dYcffrj75ZdfSr3N4MGDXUZGRqGTssH6u+GGG4qtW732S8P7o3Lps6poJjqdd955Ca/P+yN6X375pTvkkENcy5Yt/bp/8803C12u1izXX3+9a9GihcvJyXH77LOPmzx5cuR/i1C2TNauXeuuvPJK//e6Tp06/jonn3yy++OPPyL//EPZMpFTTz212Prdf//9k65C3ieVk0eivys63XnnnSXeJ++Ryv/Ou2rVKv/3vVGjRq5u3bruqKOOcn/++Wep9xtU8G9QdUGxbcwrr7ziLrnkEt9x/IcffnDdunVz++23n5s3b17C63/zzTeud+/e7owzznBjxozxbwydfvrppypf9uroiy++8B8q3377rfv444/9l6R9993XLV++vNTb5ebmujlz5sROM2bMqLJlru66du1aaN1+9dVXJV6X90flGzlyZKE89D6RY445psTb8P6Ilj6P9LdCX/oTueOOO9wDDzzgHnvsMffdd9/5Ak9/V/SlKaq/RSh7JitWrPDr9LrrrvM/hw4d6r/QHnrooZF+/qHsmYRUXMev35deeqnU1cj7pPLyiM9Bp6efftoX2yruSsN7pHK/81588cXunXfe8QMZur42Eh555JGl3u8dFfgbVK2oGzns2HHHHYPzzjsv9nt+fn7QsmXL4NZbb014/WOPPTY46KCDCp3XvXv34Oyzz670Zd0QzZs3T937gy+++KLE6wwaNCjIy8ur0uXaUPTv3z/o1q1bma/P+6PqXXTRRUHHjh2DgoKChJfz/qhc+nx64403Yr8rh+bNmwd33nln7LxFixYFtWrVCl566aXI/hah7Jkk8v333/vr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wQdu2LBhsfPy8vJcTk5Oylb3Aw884E477TSXmbnhbOOvWbOmO/744/1z32233VK9OACAFNhw/uoBAFAFvvjiC7fjjju6WrVquRYtWrirrrrKrVu3Lna5CuFdd93V1a9f3zVq1MgdfPDBburUqYXu4/vvv3fbbLONq127ttt+++3dmDFjSny8rKws17x589ipbt26Ljs7u9B5KrSLTiNv166du+mmm9zJJ5/sb9O2bVs/Aj1//nx32GGH+fO22morN2rUqEKP99VXX/niUfepQv/CCy90y5cvL3H5dH+ffvppoSnVQRD4Kdkbb7yxX08tW7b09xNavXq1u+yyy1yrVq1cnTp1XPfu3d3nn39e6H6//vpr/5w22mgj16BBA7fffvu5v//+O3Z73V/Tpk39OtT6HjlyZOy2ui+N+H/yySd+/eo+dt55Z/fLL78UeozbbrvNNWvWzNWrV8+dccYZbtWqVYUu1/0oay2j8txll13cjBkzYpfrOWudrly5ssT1AwCovii2AQCIyOzZs92BBx7odthhBzd27Fj36KOPuqeeesoXtSEVppdccokvYlXsabT3iCOOcAUFBf7yZcuW+QJ88803d6NHj/ZFqQrPynDvvff6AlHF/EEHHeROOukkX3yfeOKJfgp0x44d/e8qjkUbBfbff3931FFHuXHjxrlXXnnFF9/nn39+iY+hy1XMdunSJXbe66+/7h974MCBbvLkye7NN990W265Zexy3d+IESPcyy+/7B/nmGOO8Y+r64r2Xe/Zs6dfR7qeHkOFbX5+vr/8iiuu8I/xzDPP+OexySab+GL8r7/+KrRs1157rbv77rt9FtpAoanfoSFDhvh1f8stt/jLteHkkUceiV2uDSjanWCPPfbwy6jl6NOnjy/iQyrkdb3vvvtuPZMCAKSlAAAAlNkpp5wSHHbYYQkvu+aaa4LOnTsHBQUFsfMefvjhoG7dukF+fn7C28yfP1+VbDB+/Hj/+8CBA4NGjRoFK1eujF3n0Ucf9dcZM2ZM0uXr379/0K1bt2Ln77HHHsFFF10U+71t27bBiSeeGPt9zpw5/jGuu+662HkjRozw5+kyOeOMM4I+ffoUut/hw4cHmZmZhZY33r333ht06NCh0Hl33313sOmmmwZr1qwpdv0ZM2YEWVlZwezZswud37Nnz+Dqq6/2/+/du3ewyy67JHy8ZcuWBTVq1AheeOGF2Hl6nJYtWwZ33HGH//2zzz7zz2vYsGGx67z33nv+vPB59OjRI+jbt2+h++7evXts3S5cuNBf//PPPw9K06BBg2Dw4MGlXgcAUD0xsg0AQEQmTpzoevToUWh0UyPHGq2eNWuW/12js71793YdOnRwubm5fjq3/P7777H70PRtTX8O6T4rgx4npOnSEj/CHJ43b948/1Oj9Wq2pinm4UkjxhqVnz59esLH0BTq+OciGqnW+VoHZ511lnvjjTdiU+3Hjx/vR6g33XTTQo+j6fnhdPtwZDsRXWft2rV+vYdq1Kjhp3tr3Zb0/DVyHf9cdV1NX48Xn0PDhg19szw9f42q33///YX2nQ9puv2KFSsSLisAoHqjQRoAAFVIhZn2j37iiSf8vsoqVLfYYgu3Zs2aKs9BRWgo3ECQ6Lz4Ke5nn312of2rQ9r/OpHGjRvH9qUOaV9v7R+tJm4ff/yx69u3r7vzzjt9Qa3H0H7omkKvn/FUdEtUzd5Ke65lMWjQIL8utB++ptT/+9//9s9np512il1HU9ebNGkSyfICANILI9sAAERE+yVr391wH+ewkZcabLVu3dotXLjQF5kqyjQyq+sXLUR1nvYBjm/G9e2335rIaNttt3U///yz3we66EndtxNRo7e5c+cWe54qmLXhQd261WhM602j2rq+RrY1wlz0MdTsLRyR1v7uiWg/cy2L1ntII91qkKZ9vMtKORTd1zpRDlreq6++2n3zzTd+o0n8scQ1yq4cdR0AwIaHYhsAgHJavHixn8ocf5o5c6YfodXPCy64wE2aNMm99dZbrn///r4hmhqhqWu2OpA//vjjbsqUKb5Lty6Lp8NFaZRV06tV2L7//vv+cF4WXHnllb6oVAMzPWdNiddzLK1BmgpNjW7HF7+aiq7GcT/99JObNm2ae/75533xrRF/TR8/4YQTfGM2HRtc09PVnf3WW2917733nr+9ilsVz1rf2jChda1mdAsWLPCdwc8991x3+eWX+xFnrUOtS03lVkfxsrrooovc008/7Uevf/31V5/jhAkTYpdrubQc2kigDuQfffSRXx/xjeB0jG1NldcGAADAhodp5AAAlJNGYouOVqqQe/LJJ31xrEKvW7dufr9ena+RbFHBrQ7bmnqsUdDOnTv7kV0dwip+qvQ777zjzjnnHP8YGo29/fbbfQfwVNOIsqZ6q4u3Dv+lEXwVkscdd1yJt9FUcB1j+4UXXvBd1kWHydJhtbShQaPY2k9cz1kbIkQFrjq4X3rppb7Du4p1Tc0Ob6+CXMXtNddc4/fFVqGu/au1L7zovjUdXN3Vly5d6ruCf/jhh35jR1npOWlkWp3NNTqt9a8iXvcj6rCuIl8dzzVjQft8n3feeX6afeill17yhT4AYMOUoS5pqV4IAABQfWkaedeuXf1huDR6vSHQKPjee+/tR8Xz8vJSvTgAgBRgGjkAAKhU2tda08bDjusbAnUmf/bZZym0AWADxsg2AAAAAAARY2QbAAAAAICIUWwDAAAAABAxim0AAAAAACJGsQ0AAAAAQMQotgEAAAAAiBjFNgAAAAAAEaPYBgAAAAAgYhTbAAAAAABEjGIbAAAAAICIUWwDAAAAABCx/wNYKolsZy2XPgAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "# Data for plotting\n", + "labels = ['Kloppy', 'Fast-Forward', 'TDC (Rust)']\n", + "times = [kloppy_load_time, fff_load_time, tdc_load_time]\n", + "colors = ['#f87171', '#fbbf24', '#34d399'] # Red, Yellow, Green\n", + "\n", + "fig, ax = plt.subplots(figsize=(10, 4))\n", + "bars = ax.barh(labels, times, color=colors)\n", + "\n", + "# Add titles and labels\n", + "ax.set_xlabel('Load Time (seconds)')\n", + "ax.set_title('Benchmark: Sportec XML Loading Performance')\n", + "ax.grid(axis='x', linestyle='--', alpha=0.7)\n", + "\n", + "# Add value labels to the end of each bar\n", + "for bar in bars:\n", + " width = bar.get_width()\n", + " ax.text(width + 0.2, bar.get_y() + bar.get_height()/2, \n", + " f'{width:.2f}s', ha='left', va='center', fontweight='bold')\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -196,7 +244,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -204,11 +252,11 @@ "output_type": "stream", "text": [ "--- to_polars benchmark ---\n", - "TDC to_polars: 13.31ms\n", - "Fast-Forward tracking access: 0.02ms\n", - "Kloppy to_df(engine=\"polars\"): 1133.28ms\n", + "TDC to_polars: 6.66ms\n", + "Fast-Forward tracking access: 0.01ms\n", + "Kloppy to_df(engine=\"polars\"): 1076.91ms\n", "\n", - "Speedup TDC vs Kloppy: 85.2x\n" + "Speedup TDC vs Kloppy: 161.7x\n" ] } ], @@ -235,6 +283,46 @@ "print(f\"\\nSpeedup TDC vs Kloppy: {kloppy_conv_time / tdc_conv_time:.1f}x\")" ] }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAEiCAYAAADklbFjAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAATIZJREFUeJzt3QeYU1X+//EzM/Q2gID0KktvKiKKomJDVhFcXcsiiooKKtZVbGBBVESxoeiuYAE7NlZEVxALXWlKkV4EpDMg0mbu//mc/d38b0IyuRlyMwPzfj1PGJLcJPeefHOS72k3zXEcxwAAAAAAgKRLT/5TAgAAAAAAkm4AAAAAAAJETzcAAAAAAAEh6QYAAAAAICAk3QAAAAAABISkGwAAAACAgJB0AwAAAAAQEJJuAAAAAAACQtINAAAAAEBASLoBAFF98803Ji0tzXzwwQeHRQkNHDjQ7u/mzZtNQaX9036i4MbPkW7mzJnmpJNOMqVLl7bHO2fOnPzeJQA44pF0A0A+GDVqlP3B671UqVLFnH766Wb8+PG8JwXEVVddFfYelStXzrRq1coMHTrU7N271xwJli1bZq6//npTv359U6JECXuMJ598snn22WfNn3/+md+7Vyh4Yyw9Pd1Ur17dnH322bbhK5n2799vLr74YrN161bzzDPPmDfffNPUqVMnqa8BADhYkSi3AQBS5OGHHzb16tUzjuOY33//3Sbj5513nvnss8/MX//6V96HAqB48eLmX//6l/3/9u3bzYcffmjuvPNO22P4zjvvmMPZf/7zH5uE6RivvPJK07x5c7Nv3z7z/fffm7vuusv88ssv5pVXXjGFwf3332/uueeefHv9s846y74HqgtWrFhhhg8fbs444wz7HnXu3DlpDSyrVq0yr776qrn22muT8pwAgPhIugEgH+nH9PHHHx+6fs0115ijjz7avP322yTdPv3xxx92qGxQihQpYv7xj3+Ervfp08e0a9fOvPvuu+bpp5+2vZKH43Ersbv00kttT+fEiRNNtWrVQvf17dvXLF261CZ8h7M9e/aYYsWK2d5jP++zLvnlL3/5S1icdevWzbRs2dIMGzbskJNuN1Y2btxor5cvX/6Q9zfyuQEAsTG8HAAKEP0YLlmy5EE//nNycuyP72bNmtkhwErMNSR427ZtYdvVrVvXJuvqqTzhhBPstho2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X7582ArUOPLdeOONTmZmpvPNN98469evD112794d2uaGG25wateu7UycONGZNWuW0759e3txHThwwGnevLlz9tlnO3PmzHG++OILp3Llyk7//v3z6aiQSt7Vy4V4gWvGjBlOkSJFnEGDBjlLlixxRo8e7ZQqVcp56623Qts8/vjj9rvnk08+cebNm+d07drVqVevnvPnn3+Gtjn33HOdNm3aONOnT3e+//57u3L+ZZddRkEfYXr27OnUqFHDGTdunLNixQpn7NixTqVKlZx//vOfoW2Il8J91ozZs2fbi9Kbp59+2v5/1apVSYsNrXh+9NFHOz169HB+/vln+1tZddaIESPy5ZhxeCHpRkzPP/+8TaaKFStmTyE2bdo0SquQ0RdXtMvIkSND2+gLq0+fPvY0Gvry6datm03MvVauXOl07tzZKVmypP2RdMcddzj79+/PhyNCfifdxAu8PvvsM9sop0bexo0bO6+88krY/TrVzwMPPGB/6GqbTp06OYsXLw7bZsuWLfaHcZkyZeypCK+++mr7AxxHlqysLFuX6HdJiRIlnPr16zv33Xdf2OmbiJfCa9KkSVF/r6ixJpmxMXfuXHuqQz2HGoGUzAN+pOkfHx3iAAAAAAAgQczpBgAAAAAgICTdAAAAAAAEhKQbAAAAAICAkHQDAAAAABAQkm4AAAAAAAJC0g0AAAAAQEBIugEAAAAAIOkGAAAAAODwQk83AAAAAAABIekGACCfrFy50qSlpZk5c+bk63swcOBA07p163x7/QceeMD07t37kJ7jm2++sWW5fft2e33UqFGmfPnyMY/xqquuMhdeeKE5nEUec24WLFhgatasaf7444+U7BsA4P8j6QYAFGgbNmwwN998s6lfv74pXry4qVWrljn//PPN119/bQ53Opb169eb5s2bB/YadevWtYlZrIuSzzvvvDPfylPv77PPPmvuu+++Q3qek046yZZlZmamr+31mkrMC4umTZuaE0880Tz99NP5vSsAUOgUye8dAAAgt57gk08+2fZYDhkyxLRo0cLs37/fTJgwwfTt29csWrSoQBee9rVo0aIx78/IyDBVq1YNdB9mzpxpsrOz7f+nTJliLrroIrN48WJTrlw5e1vJkiVNmTJl7CU//Otf/7IJc506dQ7peYoVK5ZQWfpNzo8kV199tbnuuutM//79TZEi/AQEgFShpxsAUGD16dPH9sbOmDHDJot/+ctfTLNmzcztt99upk2bFtpu9erVpmvXrjZxVDJ5ySWXmN9///2gocVvvvmm7flVwnXppZeanTt32vtfeeUVU716dZOTkxP2+nrOXr16ha5/8skn5thjjzUlSpSwPe8PPfSQOXDgQOh+7etLL71kLrjgAlO6dGkzaNAgs23bNnPFFVeYypUr2wS3YcOGZuTIkTGHl0+ePNmccMIJtle/WrVq5p577gl7jdNOO83ccsst5p///KepWLGiTTR1fLHodbWNLtpeqlSpErpNZRFr6PVjjz1mjj76aNvo8fDDD9v9uOuuu+zzaKiyexyuNWvW2LLX9tpG5adjzM0777xjRy546Rg1uuHWW281FSpUsPvw6quv2qHRShzLli1rjjnmGDN+/Pg8DbX2HqPrgw8+sI06eo+OOuooc+aZZ4YNxVbjQJMmTex737hxYzN8+PCw51u7dq257LLL7HHrvT/++OPN9OnTQ/crLho0aGAbBxo1amRj0Uv7rtfo1q2bKVWqlI2TTz/9NGybzz//3H4GtI+nn376QWW7atUqW5YqM+2DPit6jOuss84yW7dutTEGAEgdkm4AQIGk5OCLL76wPdpKICK583WVKCu5c5OJr776yixfvtz8/e9/D9t+2bJl5uOPPzbjxo2zF237+OOP2/suvvhis2XLFjNp0qSDXl8Js3z33XfmyiuvNP369bPzY0eMGGGHJyux9lICq8Rp/vz5NmHXfGVtrwRx4cKFNvmqVKlS1GP+7bffzHnnnWfatm1r5s6da7f997//bR599NGw7V5//XVbJkrqnnzySZsQ67iTaeLEiWbdunXm22+/tUOSBwwYYP7617/ahE6ve8MNN5jrr7/eJptur/4555xjE2KV1Q8//GAbQc4991yzb9++qK+hMlbZKEGNpGNUOanBRQn4jTfeaN8n9Yr/9NNP5uyzzzY9evQwu3fvPuRj1bB0Jcx6v/QeKYHv3r27cRzH3j969Gjz4IMP2vda96sxQu+r9lF27dplOnbsaN8/Jcp679Qo4jbifPTRRzZu7rjjDvPzzz/bclPjgTfeRI04arSYN2+ejQPFnsrIbdDQPimpViPNtddeaxtkvPRZ2bt3r33PFH9PPPFE2AgGJfxqXNH7AwBIIQcAgAJo+vTpynicsWPH5rrdl19+6WRkZDirV68O3fbLL7/Yx86YMcNeHzBggFOqVCknKysrtM1dd93ltGvXLnS9a9euTq9evULXR4wY4VSvXt3Jzs621zt16uQ89thjYa/95ptvOtWqVQtd12veeuutYducf/75ztVXXx1131esWGEfM3v2bHv93nvvdRo1auTk5OSEtnnxxRedMmXKhPajY8eOTocOHcKep23bts7dd9/txDNp0iT7etu2bQu7XeXTqlWr0PWePXs6derUCb2maL9OOeWU0PUDBw44pUuXdt5+++1QWUTu+969e52SJUs6EyZMiLo/Om7tj/e9i3aM7mv16NEjdNv69evtY6dOnRr12EaOHOlkZmbmeox6z+XHH3+0j125cmXU/WzQoIEzZsyYsNseeeQRp3379qFYKVu2rLNly5aojz/ppJOc6667Luy2iy++2DnvvPNC1/X6999/f+j6rl277G3jx4+31/v37+80bdo07Dn0nnuPuUWLFs7AgQOd3HTr1s256qqrct0GAJBc9HQDAAokt5cxHvU8akEyXbyLRqknXPe5NKxcvbAuDd3euHFj6Lp6FT/88EPbU+j2bmoIenr6/74q1XupHmV3/rMumh+rXlJvb2tkr616aDWEWj2M6v3UvOrcjqV9+/Z2qLFLc9rVk+r2KEvLli3DHhd5LMmgocnusYuGeGv4tXc+uoZhu6+r8lm6dKktY7d8NNR6z549dpRBNH/++af9qyHbkbzH6L6W9/W1P5KM427VqpXp1KmTfX71pmsou6YFiIaYa/+vueaasPdeow/c41LPc5s2bULD96O9r3ofvXTdG5+Rx6yRDJoq4R6ftm3Xrl3Y9ooVL0070H7puTUyQT3mkTQ0PRmjAwAA/rGKBgCgQNKcViWfyVosLXJBMz23dw63hu0q0f/Pf/5jh3drCO4zzzwTul+Jr4b/aohvJG/SGDkUvnPnznaurebWagi4kjsNA37qqacCO5ZkiPYaub2uyue4446zjRXR5pVH4w6zV4IbuU2813cbJpJx3Erq9d6oQeTLL780zz//vF1NXcPoNb9alIhHJr16nJvIJsOhvq8acq4h/ophHcfgwYPN0KFD7fB8l4ara245ACB16OkGABRI6jVUAvHiiy9GPbewu2CWFrfSfFddXJonrPvV4+2XEmcl1Eoa3377bbvYlRZNc+n/WvVbC3hFXrw9wtEooezZs6d56623zLBhw+zCbdHoWKZOnRrWy6+50eo91sJlBZnKZ8mSJXaRtsjyibVSuJI/9ebq/cpvSnDVQ6yGldmzZ9v5z5qLrR51LbKndQIij6tevXqhHmr1drvzr6O9r3ofvXQ9kfjUc2h+u5d3MUGXRnxovv3YsWPtHHI1FnhpTrl65QEAqUPSDQAosJRw63RXWs1bQ7+V1GmY7XPPPRcaWqtVpjUsWMPDtcCWEhMteKaFraIt0JUbPYd6CV977bXQAmouLaT1xhtv2KTsl19+sfuhYeP3339/rs+px2nVcw291uO0iJsSqFirtavxQD2T6uHX4zRMWKu1x0vs85vKSz3XWtROowRWrFhhFyTTkGfv0HgvHZPev++//97kJ/Voa3G0WbNm2ZXwlbBu2rQp9D7pPVevseLu119/tYuUaeV295zXWoRNK8FrNXQl00rQFa9qQBGt+K5F97QwnmJYj9Nr6PzofimR1mP1XGr8GTNmzEHnGddq7zqdnspenwUt1OaNNa12rsXeVOYAgNQp2N/gAIBCTaflUvKg0yOp16558+b2tEdff/21TWDcHkolp1pV+9RTT7UJhR737rvvJvx6Z5xxhu1hV1Jz+eWXh92nXnclzBq2q+HnJ554oh1+Hu/80uox1XmR1Ruq/dOQZCXr0dSoUcMOQ1fDgeYZK9HSXOJ4iX1BoGHYWjW7du3adsSAkj3tu+Z0u+cEjzUkWuWR7OHxidD+ad+1YrhOyaXy1rBsTQ1w91Gn81KirQYeNego4XV7uvUeKy7Uy6/n0DZaGd8dfq5k/Nlnn7VTCjRXXivf67l0ajS/VK5K5LUCv2Lj5Zdftg0FXmqg0tQFlb1WjdexeE9tphEcWvX9UM+JDgBITJpWU0vwMQAAAEmhnyGaK33bbbfZHmMEQ6dt0zoJ6iGPXNQNABAseroBAEC+0UgFzXE/cOAA70KANGz+3nvvJeEGgHxATzcAAAAAAAGhpxsAAAAAgICQdAMAAAAAEBCSbgAAAAAAAkLSDQAAAABAQEi6AQAAAAAICEk3AAAAAAABIekGAAAAACAgJN0AAAAAAASEpBsAAAAAABOM/wf/CXHgKwb+QgAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Data for plotting\n", + "labels = ['Kloppy', 'TDC (Zero-Copy)']\n", + "times = [kloppy_conv_time * 1000, tdc_conv_time * 1000]\n", + "colors = ['#f87171', '#34d399'] # Red, Green\n", + "\n", + "fig, ax = plt.subplots(figsize=(10, 3))\n", + "bars = ax.barh(labels, times, color=colors)\n", + "\n", + "# Add titles and labels\n", + "ax.set_xlabel('Conversion Time (milliseconds)')\n", + "ax.set_title('Benchmark: Polars Conversion Performance')\n", + "ax.grid(axis='x', linestyle='--', alpha=0.7)\n", + "\n", + "# Add value labels\n", + "for bar in bars:\n", + " width = bar.get_width()\n", + " ax.text(width + 5, bar.get_y() + bar.get_height()/2, \n", + " f'{width:.2f}ms', ha='left', va='center', fontweight='bold')\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/examples/03-interop.ipynb b/examples/03-interop.ipynb index c363521..03e1c06 100644 --- a/examples/03-interop.ipynb +++ b/examples/03-interop.ipynb @@ -49,7 +49,7 @@ " origin=tdc.Origin.TopLeft,\n", " length_unit=tdc.LengthUnit.Percent,\n", ")\n", - "container = tdc.TrackingDataContainer(data).with_coordinate_system(cs)\n", + "container = tdc.TrackingDataContainer(data, validate=False).with_coordinate_system(cs)\n", "print(f\"Container created with {container.num_rows()} rows.\")" ] }, diff --git a/examples/04-udfs.ipynb b/examples/04-udfs.ipynb index 9de72b3..fb992ac 100644 --- a/examples/04-udfs.ipynb +++ b/examples/04-udfs.ipynb @@ -68,7 +68,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Expression Engine time: 0.5881s\n" + "Expression Engine time: 0.4595s\n" ] } ], @@ -97,7 +97,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Polars Vectorized time: 0.3581s\n" + "Polars Vectorized time: 0.4631s\n" ] } ], @@ -123,7 +123,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Numpy Vectorized time: 0.0208s\n" + "Numpy Vectorized time: 0.0394s\n" ] } ], @@ -161,7 +161,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Optimized Rust Kernel time: 0.0463s\n" + "Optimized Rust Kernel time: 0.0359s\n" ] } ], @@ -222,7 +222,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Native Polars Batch time: 0.9378s\n" + "Native Polars Batch time: 0.8644s\n" ] } ], @@ -289,6 +289,13 @@ "if mismatch_polars < 1e-6 and mismatch_rust < 1e-6:\n", " print(\"\\nVerification Successful: All methods produced identical results.\")" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/examples/05-lazy.ipynb b/examples/05-lazy.ipynb index a489553..b25ecbd 100644 --- a/examples/05-lazy.ipynb +++ b/examples/05-lazy.ipynb @@ -126,7 +126,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Collected 1500 frames in 0.0050s.\n", + "Collected 1500 frames in 0.0165s.\n", "shape: (5, 6)\n", "┌──────────┬────────┬────────┬──────────┬────────┬──────────┐\n", "│ frame_id ┆ ball/z ┆ ball/y ┆ ball/vel ┆ ball/x ┆ ball/acc │\n", diff --git a/examples/06-skeletal.ipynb b/examples/06-skeletal.ipynb index 540fb80..36dbb21 100644 --- a/examples/06-skeletal.ipynb +++ b/examples/06-skeletal.ipynb @@ -58,8 +58,8 @@ "output_type": "stream", "text": [ "Found 95 minutes of data.\n", - "Loaded 132917 frames in 25.05 seconds.\n", - "Memory used: 3578.27 MB\n", + "Loaded 132917 frames in 29.43 seconds.\n", + "Memory used: 2692.30 MB\n", "Total columns: 3552\n" ] } @@ -139,8 +139,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "Loaded 118 columns for player #10 in 0.1026 seconds.\n", - "Memory used for selection: 71.14 MB\n" + "Loaded 118 columns for player #10 in 0.4122 seconds.\n", + "Memory used for selection: -54.38 MB\n" ] } ], @@ -366,42 +366,42 @@ "\n", "\n", "
\n", - " \n", + " \n", "
\n", - " \n", + " oninput=\"anima73aa44715804f77a7b32c6bca3f75d9.set_frame(parseInt(this.value));\">\n", "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", "
\n", - "
\n", - " \n", - " \n", - " Once\n", + " \n", - " \n", - " Loop\n", + " \n", - " \n", + " \n", "
\n", "
\n", "
\n", @@ -411,9 +411,9 @@ " /* Instantiate the Animation class. */\n", " /* The IDs given should match those used in the template above. */\n", " (function() {\n", - " var img_id = \"_anim_imge9b75df42be44b978907a50a64638a19\";\n", - " var slider_id = \"_anim_slidere9b75df42be44b978907a50a64638a19\";\n", - " var loop_select_id = \"_anim_loop_selecte9b75df42be44b978907a50a64638a19\";\n", + " var img_id = \"_anim_imga73aa44715804f77a7b32c6bca3f75d9\";\n", + " var slider_id = \"_anim_slidera73aa44715804f77a7b32c6bca3f75d9\";\n", + " var loop_select_id = \"_anim_loop_selecta73aa44715804f77a7b32c6bca3f75d9\";\n", " var frames = new Array(150);\n", " \n", " frames[0] = \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAMgCAYAAACwGEg9AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90\\\n", @@ -226848,7 +226848,7 @@ " /* set a timeout to make sure all the above elements are created before\n", " the object is initialized. */\n", " setTimeout(function() {\n", - " anime9b75df42be44b978907a50a64638a19 = new Animation(frames, img_id, slider_id, 40.0,\n", + " anima73aa44715804f77a7b32c6bca3f75d9 = new Animation(frames, img_id, slider_id, 40.0,\n", " loop_select_id);\n", " }, 0);\n", " })()\n", diff --git a/examples/07-catalog.ipynb b/examples/07-catalog.ipynb index 901392e..4312109 100644 --- a/examples/07-catalog.ipynb +++ b/examples/07-catalog.ipynb @@ -56,7 +56,6 @@ " shutil.rmtree(warehouse_path)\n", "\n", "config = tdc.IcebergConfig(\n", - " bucket_count=10,\n", " compression=\"zstd\"\n", ")\n", "\n", @@ -96,6 +95,8 @@ "source": [ "data = {\n", " \"match/id\": [\"match_A\"] * 5,\n", + " \"frame_id\": list(range(5)),\n", + " \"period\": [\"first_half\"] * 5,\n", " \"timestamp\": [1000000 + i * 40000 for i in range(5)],\n", " \"ball/x\": [0.0, 1.0, 2.0, 3.0, 4.0],\n", " \"ball/y\": [0.0, 0.5, 1.0, 1.5, 2.0],\n", @@ -145,21 +146,26 @@ " white-space: pre-wrap;\n", "}\n", "\n", - "shape: (5, 6)
match/idtimestampframe_idperiodball/xball/y
strdatetime[μs]u64strf32f32
"match_A"1969-12-31 22:00:01nullnull0.00.0
"match_A"1969-12-31 22:00:01.040nullnull1.00.5
"match_A"1969-12-31 22:00:01.080nullnull2.01.0
"match_A"1969-12-31 22:00:01.120nullnull3.01.5
"match_A"1969-12-31 22:00:01.160nullnull4.02.0
" + "shape: (5, 9)
competition/idseason/idmatch/idframe_idperiodtimestampball/xball/yball/z
strstrstri64catdatetime[μs]f32f32f32
nullnull"match_A"0"first_half"1970-01-01 00:00:010.00.0null
nullnull"match_A"1"first_half"1970-01-01 00:00:01.0401.00.5null
nullnull"match_A"2"first_half"1970-01-01 00:00:01.0802.01.0null
nullnull"match_A"3"first_half"1970-01-01 00:00:01.1203.01.5null
nullnull"match_A"4"first_half"1970-01-01 00:00:01.1604.02.0null
" ], "text/plain": [ - "shape: (5, 6)\n", - "┌──────────┬─────────────────────────┬──────────┬────────┬────────┬────────┐\n", - "│ match/id ┆ timestamp ┆ frame_id ┆ period ┆ ball/x ┆ ball/y │\n", - "│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │\n", - "│ str ┆ datetime[μs] ┆ u64 ┆ str ┆ f32 ┆ f32 │\n", - "╞══════════╪═════════════════════════╪══════════╪════════╪════════╪════════╡\n", - "│ match_A ┆ 1969-12-31 22:00:01 ┆ null ┆ null ┆ 0.0 ┆ 0.0 │\n", - "│ match_A ┆ 1969-12-31 22:00:01.040 ┆ null ┆ null ┆ 1.0 ┆ 0.5 │\n", - "│ match_A ┆ 1969-12-31 22:00:01.080 ┆ null ┆ null ┆ 2.0 ┆ 1.0 │\n", - "│ match_A ┆ 1969-12-31 22:00:01.120 ┆ null ┆ null ┆ 3.0 ┆ 1.5 │\n", - "│ match_A ┆ 1969-12-31 22:00:01.160 ┆ null ┆ null ┆ 4.0 ┆ 2.0 │\n", - "└──────────┴─────────────────────────┴──────────┴────────┴────────┴────────┘" + "shape: (5, 9)\n", + "┌────────────────┬───────────┬──────────┬──────────┬───┬────────────────┬────────┬────────┬────────┐\n", + "│ competition/id ┆ season/id ┆ match/id ┆ frame_id ┆ … ┆ timestamp ┆ ball/x ┆ ball/y ┆ ball/z │\n", + "│ --- ┆ --- ┆ --- ┆ --- ┆ ┆ --- ┆ --- ┆ --- ┆ --- │\n", + "│ str ┆ str ┆ str ┆ i64 ┆ ┆ datetime[μs] ┆ f32 ┆ f32 ┆ f32 │\n", + "╞════════════════╪═══════════╪══════════╪══════════╪═══╪════════════════╪════════╪════════╪════════╡\n", + "│ null ┆ null ┆ match_A ┆ 0 ┆ … ┆ 1970-01-01 ┆ 0.0 ┆ 0.0 ┆ null │\n", + "│ ┆ ┆ ┆ ┆ ┆ 00:00:01 ┆ ┆ ┆ │\n", + "│ null ┆ null ┆ match_A ┆ 1 ┆ … ┆ 1970-01-01 ┆ 1.0 ┆ 0.5 ┆ null │\n", + "│ ┆ ┆ ┆ ┆ ┆ 00:00:01.040 ┆ ┆ ┆ │\n", + "│ null ┆ null ┆ match_A ┆ 2 ┆ … ┆ 1970-01-01 ┆ 2.0 ┆ 1.0 ┆ null │\n", + "│ ┆ ┆ ┆ ┆ ┆ 00:00:01.080 ┆ ┆ ┆ │\n", + "│ null ┆ null ┆ match_A ┆ 3 ┆ … ┆ 1970-01-01 ┆ 3.0 ┆ 1.5 ┆ null │\n", + "│ ┆ ┆ ┆ ┆ ┆ 00:00:01.120 ┆ ┆ ┆ │\n", + "│ null ┆ null ┆ match_A ┆ 4 ┆ … ┆ 1970-01-01 ┆ 4.0 ┆ 2.0 ┆ null │\n", + "│ ┆ ┆ ┆ ┆ ┆ 00:00:01.160 ┆ ┆ ┆ │\n", + "└────────────────┴───────────┴──────────┴──────────┴───┴────────────────┴────────┴────────┴────────┘" ] }, "execution_count": 4, @@ -240,21 +246,26 @@ " white-space: pre-wrap;\n", "}\n", "\n", - "shape: (5, 7)
match/idtimestampframe_idperiodball/xball/yball/z
strdatetime[μs]u64strf32f32f32
"match_A"1969-12-31 20:00:01nullnull0.00.00.5
"match_A"1969-12-31 20:00:01.040nullnull1.00.50.5
"match_A"1969-12-31 20:00:01.080nullnull2.01.00.5
"match_A"1969-12-31 20:00:01.120nullnull3.01.50.5
"match_A"1969-12-31 20:00:01.160nullnull4.02.00.5
" + "shape: (5, 9)
competition/idseason/idmatch/idframe_idperiodtimestampball/xball/yball/z
strstrstri64catdatetime[μs]f32f32f32
nullnull"match_A"0"first_half"1970-01-01 00:00:010.00.00.5
nullnull"match_A"1"first_half"1970-01-01 00:00:01.0401.00.50.5
nullnull"match_A"2"first_half"1970-01-01 00:00:01.0802.01.00.5
nullnull"match_A"3"first_half"1970-01-01 00:00:01.1203.01.50.5
nullnull"match_A"4"first_half"1970-01-01 00:00:01.1604.02.00.5
" ], "text/plain": [ - "shape: (5, 7)\n", - "┌──────────┬─────────────────────────┬──────────┬────────┬────────┬────────┬────────┐\n", - "│ match/id ┆ timestamp ┆ frame_id ┆ period ┆ ball/x ┆ ball/y ┆ ball/z │\n", - "│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │\n", - "│ str ┆ datetime[μs] ┆ u64 ┆ str ┆ f32 ┆ f32 ┆ f32 │\n", - "╞══════════╪═════════════════════════╪══════════╪════════╪════════╪════════╪════════╡\n", - "│ match_A ┆ 1969-12-31 20:00:01 ┆ null ┆ null ┆ 0.0 ┆ 0.0 ┆ 0.5 │\n", - "│ match_A ┆ 1969-12-31 20:00:01.040 ┆ null ┆ null ┆ 1.0 ┆ 0.5 ┆ 0.5 │\n", - "│ match_A ┆ 1969-12-31 20:00:01.080 ┆ null ┆ null ┆ 2.0 ┆ 1.0 ┆ 0.5 │\n", - "│ match_A ┆ 1969-12-31 20:00:01.120 ┆ null ┆ null ┆ 3.0 ┆ 1.5 ┆ 0.5 │\n", - "│ match_A ┆ 1969-12-31 20:00:01.160 ┆ null ┆ null ┆ 4.0 ┆ 2.0 ┆ 0.5 │\n", - "└──────────┴─────────────────────────┴──────────┴────────┴────────┴────────┴────────┘" + "shape: (5, 9)\n", + "┌────────────────┬───────────┬──────────┬──────────┬───┬────────────────┬────────┬────────┬────────┐\n", + "│ competition/id ┆ season/id ┆ match/id ┆ frame_id ┆ … ┆ timestamp ┆ ball/x ┆ ball/y ┆ ball/z │\n", + "│ --- ┆ --- ┆ --- ┆ --- ┆ ┆ --- ┆ --- ┆ --- ┆ --- │\n", + "│ str ┆ str ┆ str ┆ i64 ┆ ┆ datetime[μs] ┆ f32 ┆ f32 ┆ f32 │\n", + "╞════════════════╪═══════════╪══════════╪══════════╪═══╪════════════════╪════════╪════════╪════════╡\n", + "│ null ┆ null ┆ match_A ┆ 0 ┆ … ┆ 1970-01-01 ┆ 0.0 ┆ 0.0 ┆ 0.5 │\n", + "│ ┆ ┆ ┆ ┆ ┆ 00:00:01 ┆ ┆ ┆ │\n", + "│ null ┆ null ┆ match_A ┆ 1 ┆ … ┆ 1970-01-01 ┆ 1.0 ┆ 0.5 ┆ 0.5 │\n", + "│ ┆ ┆ ┆ ┆ ┆ 00:00:01.040 ┆ ┆ ┆ │\n", + "│ null ┆ null ┆ match_A ┆ 2 ┆ … ┆ 1970-01-01 ┆ 2.0 ┆ 1.0 ┆ 0.5 │\n", + "│ ┆ ┆ ┆ ┆ ┆ 00:00:01.080 ┆ ┆ ┆ │\n", + "│ null ┆ null ┆ match_A ┆ 3 ┆ … ┆ 1970-01-01 ┆ 3.0 ┆ 1.5 ┆ 0.5 │\n", + "│ ┆ ┆ ┆ ┆ ┆ 00:00:01.120 ┆ ┆ ┆ │\n", + "│ null ┆ null ┆ match_A ┆ 4 ┆ … ┆ 1970-01-01 ┆ 4.0 ┆ 2.0 ┆ 0.5 │\n", + "│ ┆ ┆ ┆ ┆ ┆ 00:00:01.160 ┆ ┆ ┆ │\n", + "└────────────────┴───────────┴──────────┴──────────┴───┴────────────────┴────────┴────────┴────────┘" ] }, "execution_count": 6, @@ -294,6 +305,13 @@ " shutil.rmtree(warehouse_path)\n", "print(\"Warehouse cleaned up.\")" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/examples/08-idsse-warehouse.ipynb b/examples/08-idsse-warehouse.ipynb index 23f1014..c428c04 100644 --- a/examples/08-idsse-warehouse.ipynb +++ b/examples/08-idsse-warehouse.ipynb @@ -138,15 +138,15 @@ "text": [ "\n", "Processing match DFL-MAT-J03WMX...\n", - " ✅ Parsed in 14.33s, Saved in 12.99s\n", + " ✅ Parsed in 1.48s, Saved in 1.36s\n", " ✅ Rows: 145,967\n", "\n", "Processing match DFL-MAT-J03WN1...\n", - " ✅ Parsed in 14.03s, Saved in 12.06s\n", + " ✅ Parsed in 1.22s, Saved in 1.26s\n", " ✅ Rows: 141,561\n", "\n", "Processing match DFL-MAT-J03WOH...\n", - " ✅ Parsed in 12.71s, Saved in 10.63s\n", + " ✅ Parsed in 1.19s, Saved in 1.08s\n", " ✅ Rows: 137,214\n" ] } @@ -242,7 +242,7 @@ "output_type": "stream", "text": [ "Loading data for DFL-MAT-J03WMX...\n", - " ✅ Loaded and pivoted 165259 rows in 7.1870s\n" + " ✅ Loaded and pivoted 165259 rows in 1.0861s\n" ] }, { @@ -255,27 +255,27 @@ " white-space: pre-wrap;\n", "}\n", "\n", - "shape: (5, 273)
competition/idseason/idmatch/idframe_idperiodtimestampteams/home/players/DFL-OBJ-0026ZI/player/idteams/home/players/DFL-OBJ-0026ZI/team/idteams/home/players/DFL-OBJ-0026ZI/xteams/home/players/DFL-OBJ-0026ZI/yteams/home/players/DFL-OBJ-0026ZI/zteams/home/players/DFL-OBJ-0026ZI/velteams/home/players/DFL-OBJ-0026ZI/accteams/home/players/DFL-OBJ-0002BO/player/idteams/home/players/DFL-OBJ-0002BO/team/idteams/home/players/DFL-OBJ-0002BO/xteams/home/players/DFL-OBJ-0002BO/yteams/home/players/DFL-OBJ-0002BO/zteams/home/players/DFL-OBJ-0002BO/velteams/home/players/DFL-OBJ-0002BO/accteams/home/players/DFL-OBJ-0027B6/player/idteams/home/players/DFL-OBJ-0027B6/team/idteams/home/players/DFL-OBJ-0027B6/xteams/home/players/DFL-OBJ-0027B6/yteams/home/players/DFL-OBJ-0027B6/zteams/home/players/DFL-OBJ-0027B6/velteams/home/players/DFL-OBJ-0027B6/accteams/away/players/DFL-OBJ-002GCR/player/idteams/away/players/DFL-OBJ-002GCR/team/idteams/away/players/DFL-OBJ-002GCR/xteams/away/players/DFL-OBJ-002GCR/yteams/away/players/DFL-OBJ-002GCR/zteams/away/players/DFL-OBJ-002GCR/velteams/away/players/DFL-OBJ-002GCR/accteams/away/players/DFL-OBJ-0027G0/player/idteams/away/players/DFL-OBJ-0027G0/team/idteams/away/players/DFL-OBJ-0027G0/xteams/home/players/DFL-OBJ-002GBW/zteams/home/players/DFL-OBJ-002GBW/velteams/home/players/DFL-OBJ-002GBW/accteams/away/player/idteams/away/team/idteams/away/xteams/away/yteams/away/zteams/away/velteams/away/accteams/home/players/DFL-OBJ-0028BD/player/idteams/home/players/DFL-OBJ-0028BD/team/idteams/home/players/DFL-OBJ-0028BD/xteams/home/players/DFL-OBJ-0028BD/yteams/home/players/DFL-OBJ-0028BD/zteams/home/players/DFL-OBJ-0028BD/velteams/home/players/DFL-OBJ-0028BD/accteams/away/players/DFL-OBJ-0002F5/player/idteams/away/players/DFL-OBJ-0002F5/team/idteams/away/players/DFL-OBJ-0002F5/xteams/away/players/DFL-OBJ-0002F5/yteams/away/players/DFL-OBJ-0002F5/zteams/away/players/DFL-OBJ-0002F5/velteams/away/players/DFL-OBJ-0002F5/accteams/away/players/DFL-OBJ-J015S4/player/idteams/away/players/DFL-OBJ-J015S4/team/idteams/away/players/DFL-OBJ-J015S4/xteams/away/players/DFL-OBJ-J015S4/yteams/away/players/DFL-OBJ-J015S4/zteams/away/players/DFL-OBJ-J015S4/velteams/away/players/DFL-OBJ-J015S4/accteams/away/jersey_colourteams/away/nameteams/away/idteams/home/jersey_colourteams/home/nameteams/home/id
strstrstri64strdatetime[μs]strstrf32f32f32f64f64strstrf32f32f32f64f64strstrf32f32f32f64f64strstrf32f32f32f64f64strstrf32f32f64f64strstrf32f32f32f64f64strstrf32f32f32f64f64strstrf32f32f32f64f64strstrf32f32f32f64f64strstrstrstrstrstr
nullnull"DFL-MAT-J03WMX"10000"first_half"2023-05-27 13:30:12.600nullnullnullnullnullnullnull"DFL-OBJ-0002BO""home"17.2620.85null0.00.0"DFL-OBJ-0027B6""home"20.167.08null0.00.0nullnullnullnullnullnullnull"DFL-OBJ-0027G0""away"-12.8nullnullnull"team:away""away"nullnullnullnullnull"DFL-OBJ-0028BD""home"0.44-15.57null0.00.0"DFL-OBJ-0002F5""away"-9.09-1.96null0.00.0nullnullnullnullnullnullnull"#25282A""FC Bayern München""DFL-CLU-00000G""#F4F5F0""1. FC Köln""DFL-CLU-000008"
nullnull"DFL-MAT-J03WMX"10001"first_half"2023-05-27 13:30:12.640nullnullnullnullnullnullnull"DFL-OBJ-0002BO""home"17.2320.879999null4.431.08"DFL-OBJ-0027B6""home"20.1200017.09null2.980.74nullnullnullnullnullnullnull"DFL-OBJ-0027G0""away"-12.83nullnullnull"team:away""away"nullnullnullnullnull"DFL-OBJ-0028BD""home"0.37-15.57null6.912.7"DFL-OBJ-0002F5""away"-9.06-1.95null3.10.0nullnullnullnullnullnullnull"#25282A""FC Bayern München""DFL-CLU-00000G""#F4F5F0""1. FC Köln""DFL-CLU-000008"
nullnull"DFL-MAT-J03WMX"10002"first_half"2023-05-27 13:30:12.680nullnullnullnullnullnullnull"DFL-OBJ-0002BO""home"17.1820.91null4.61.07"DFL-OBJ-0027B6""home"20.097.1null3.090.65nullnullnullnullnullnullnull"DFL-OBJ-0027G0""away"-12.88nullnullnull"team:away""away"nullnullnullnullnull"DFL-OBJ-0028BD""home"0.29-15.57null7.312.69"DFL-OBJ-0002F5""away"-9.02-1.94null3.170.0nullnullnullnullnullnullnull"#25282A""FC Bayern München""DFL-CLU-00000G""#F4F5F0""1. FC Köln""DFL-CLU-000008"
nullnull"DFL-MAT-J03WMX"10003"first_half"2023-05-27 13:30:12.720nullnullnullnullnullnullnull"DFL-OBJ-0002BO""home"17.1520.940001null4.741.03"DFL-OBJ-0027B6""home"20.0599997.12null3.180.56nullnullnullnullnullnullnull"DFL-OBJ-0027G0""away"-12.91nullnullnull"team:away""away"nullnullnullnullnull"DFL-OBJ-0028BD""home"0.21-15.57null7.692.66"DFL-OBJ-0002F5""away"-8.99-1.93null3.290.0nullnullnullnullnullnullnull"#25282A""FC Bayern München""DFL-CLU-00000G""#F4F5F0""1. FC Köln""DFL-CLU-000008"
nullnull"DFL-MAT-J03WMX"10004"first_half"2023-05-27 13:30:12.760nullnullnullnullnullnullnull"DFL-OBJ-0002BO""home"17.11000120.98null4.880.98"DFL-OBJ-0027B6""home"20.027.13null3.250.44nullnullnullnullnullnullnull"DFL-OBJ-0027G0""away"-12.94nullnullnull"team:away""away"nullnullnullnullnull"DFL-OBJ-0028BD""home"0.12-15.57null8.072.62"DFL-OBJ-0002F5""away"-8.96-1.91null3.440.0nullnullnullnullnullnullnull"#25282A""FC Bayern München""DFL-CLU-00000G""#F4F5F0""1. FC Köln""DFL-CLU-000008"
" + "shape: (5, 241)
competition/idseason/idmatch/idframe_idperiodtimestampball_statusball_poss_team_idteams/none/players/DFL-OBJ-002G4A/player/idteams/none/players/DFL-OBJ-002G4A/xteams/none/players/DFL-OBJ-002G4A/yteams/none/players/DFL-OBJ-002G4A/zteams/none/players/DFL-OBJ-002G4A/accteams/none/players/DFL-OBJ-002G4A/velteams/none/players/DFL-OBJ-0000IA/player/idteams/none/players/DFL-OBJ-0000IA/xteams/none/players/DFL-OBJ-0000IA/yteams/none/players/DFL-OBJ-0000IA/zteams/none/players/DFL-OBJ-0000IA/accteams/none/players/DFL-OBJ-0000IA/velteams/none/players/DFL-OBJ-0002HE/player/idteams/none/players/DFL-OBJ-0002HE/xteams/none/players/DFL-OBJ-0002HE/yteams/none/players/DFL-OBJ-0002HE/zteams/none/players/DFL-OBJ-0002HE/accteams/none/players/DFL-OBJ-0002HE/velteams/none/players/DFL-OBJ-0027V2/player/idteams/none/players/DFL-OBJ-0027V2/xteams/none/players/DFL-OBJ-0027V2/yteams/none/players/DFL-OBJ-0027V2/zteams/none/players/DFL-OBJ-0027V2/accteams/none/players/DFL-OBJ-0027V2/velreferees/DFL-OBJ-000141/player/idreferees/DFL-OBJ-000141/xreferees/DFL-OBJ-000141/yreferees/DFL-OBJ-000141/zreferees/DFL-OBJ-000141/accreferees/DFL-OBJ-00011J/velteams/none/players/DFL-OBJ-J015S4/player/idteams/none/players/DFL-OBJ-J015S4/xteams/none/players/DFL-OBJ-J015S4/yteams/none/players/DFL-OBJ-J015S4/zteams/none/players/DFL-OBJ-J015S4/accteams/none/players/DFL-OBJ-J015S4/velteams/away/player/idteams/away/xteams/away/yteams/away/zteams/away/accteams/away/velteams/none/players/DFL-OBJ-J01BGM/player/idteams/none/players/DFL-OBJ-J01BGM/xteams/none/players/DFL-OBJ-J01BGM/yteams/none/players/DFL-OBJ-J01BGM/zteams/none/players/DFL-OBJ-J01BGM/accteams/none/players/DFL-OBJ-J01BGM/velteams/none/players/DFL-OBJ-0027G0/player/idteams/none/players/DFL-OBJ-0027G0/xteams/none/players/DFL-OBJ-0027G0/yteams/none/players/DFL-OBJ-0027G0/zteams/none/players/DFL-OBJ-0027G0/accteams/none/players/DFL-OBJ-0027G0/velteams/none/players/DFL-OBJ-0002AU/player/idteams/none/players/DFL-OBJ-0002AU/xteams/none/players/DFL-OBJ-0002AU/yteams/none/players/DFL-OBJ-0002AU/zteams/none/players/DFL-OBJ-0002AU/accteams/none/players/DFL-OBJ-0002AU/velteams/home/jersey_colourteams/home/nameteams/home/idteams/away/jersey_colourteams/away/nameteams/away/id
strstrstri64catdatetime[μs]boolstrstrf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strstrstrstrstrstr
nullnull"DFL-MAT-J03WMX"10000"first_half"2023-05-27 13:30:12.600null"DFL-CLU-00000G""DFL-OBJ-002G4A"10.39-3.39null0.00.0"DFL-OBJ-0000IA"-1.5312.68null0.00.0"DFL-OBJ-0002HE"47.3699990.25null0.00.0nullnullnullnullnullnull"referee:DFL-OBJ-000141"1.218.15null0.00.0nullnullnullnullnullnull"team:away"nullnullnullnullnull"DFL-OBJ-J01BGM"1.413.05null0.00.0"DFL-OBJ-0027G0"-12.8-18.85null0.00.0"DFL-OBJ-0002AU"0.11-21.870001null0.00.0"#F4F5F0""1. FC Köln""DFL-CLU-000008""#25282A""FC Bayern München""DFL-CLU-00000G"
nullnull"DFL-MAT-J03WMX"10001"first_half"2023-05-27 13:30:12.640null"DFL-CLU-00000G""DFL-OBJ-002G4A"10.28-3.39null1.2210.72"DFL-OBJ-0000IA"-1.4512.68null0.011.34"DFL-OBJ-0002HE"47.340.26null0.423.56nullnullnullnullnullnull"referee:DFL-OBJ-000141"1.2818.129999null0.670.19nullnullnullnullnullnull"team:away"nullnullnullnullnull"DFL-OBJ-J01BGM"1.3913.05null0.150.7"DFL-OBJ-0027G0"-12.83-18.860001null0.03.04"DFL-OBJ-0002AU"0.08-21.870001null0.01.73"#F4F5F0""1. FC Köln""DFL-CLU-000008""#25282A""FC Bayern München""DFL-CLU-00000G"
nullnull"DFL-MAT-J03WMX"10002"first_half"2023-05-27 13:30:12.680null"DFL-CLU-00000G""DFL-OBJ-002G4A"10.16-3.39null1.1310.9"DFL-OBJ-0000IA"-1.2712.65null0.012.8"DFL-OBJ-0002HE"47.2999990.26null0.373.62nullnullnullnullnullnull"referee:DFL-OBJ-000141"1.3418.09null0.610.32nullnullnullnullnullnull"team:away"nullnullnullnullnull"DFL-OBJ-J01BGM"1.3913.05null0.670.76"DFL-OBJ-0027G0"-12.88-18.860001null0.03.17"DFL-OBJ-0002AU"0.04-21.860001null0.02.03"#F4F5F0""1. FC Köln""DFL-CLU-000008""#25282A""FC Bayern München""DFL-CLU-00000G"
nullnull"DFL-MAT-J03WMX"10003"first_half"2023-05-27 13:30:12.720null"DFL-CLU-00000G""DFL-OBJ-002G4A"10.03-3.39null1.0311.06"DFL-OBJ-0000IA"-1.0712.64null0.014.26"DFL-OBJ-0002HE"47.2599980.26null0.343.68nullnullnullnullnullnull"referee:DFL-OBJ-000141"1.4118.059999null0.550.47nullnullnullnullnullnull"team:away"nullnullnullnullnull"DFL-OBJ-J01BGM"1.3813.06null1.190.9"DFL-OBJ-0027G0"-12.91-18.870001null0.03.31"DFL-OBJ-0002AU"0.01-21.85null0.02.57"#F4F5F0""1. FC Köln""DFL-CLU-000008""#25282A""FC Bayern München""DFL-CLU-00000G"
nullnull"DFL-MAT-J03WMX"10004"first_half"2023-05-27 13:30:12.760null"DFL-CLU-00000G""DFL-OBJ-002G4A"9.91-3.39null0.9211.19"DFL-OBJ-0000IA"-0.8812.61null0.015.7"DFL-OBJ-0002HE"47.2200010.27null0.323.73nullnullnullnullnullnull"referee:DFL-OBJ-000141"1.4918.02null0.470.63nullnullnullnullnullnull"team:away"nullnullnullnullnull"DFL-OBJ-J01BGM"1.3713.06null1.671.11"DFL-OBJ-0027G0"-12.94-18.870001null0.03.47"DFL-OBJ-0002AU"-0.01-21.85null0.01.87"#F4F5F0""1. FC Köln""DFL-CLU-000008""#25282A""FC Bayern München""DFL-CLU-00000G"
" ], "text/plain": [ - "shape: (5, 273)\n", + "shape: (5, 241)\n", "┌───────────┬───────────┬───────────┬──────────┬───┬───────────┬───────────┬───────────┬───────────┐\n", - "│ competiti ┆ season/id ┆ match/id ┆ frame_id ┆ … ┆ teams/awa ┆ teams/hom ┆ teams/hom ┆ teams/hom │\n", - "│ on/id ┆ --- ┆ --- ┆ --- ┆ ┆ y/id ┆ e/jersey_ ┆ e/name ┆ e/id │\n", + "│ competiti ┆ season/id ┆ match/id ┆ frame_id ┆ … ┆ teams/hom ┆ teams/awa ┆ teams/awa ┆ teams/awa │\n", + "│ on/id ┆ --- ┆ --- ┆ --- ┆ ┆ e/id ┆ y/jersey_ ┆ y/name ┆ y/id │\n", "│ --- ┆ str ┆ str ┆ i64 ┆ ┆ --- ┆ colour ┆ --- ┆ --- │\n", "│ str ┆ ┆ ┆ ┆ ┆ str ┆ --- ┆ str ┆ str │\n", "│ ┆ ┆ ┆ ┆ ┆ ┆ str ┆ ┆ │\n", "╞═══════════╪═══════════╪═══════════╪══════════╪═══╪═══════════╪═══════════╪═══════════╪═══════════╡\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10000 ┆ … ┆ DFL-CLU-0 ┆ #F4F5F0 ┆ 1. FC ┆ DFL-CLU-0 │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ 0000G ┆ ┆ Köln ┆ 00008 │\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10001 ┆ … ┆ DFL-CLU-0 ┆ #F4F5F0 ┆ 1. FC ┆ DFL-CLU-0 │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ 0000G ┆ ┆ Köln ┆ 00008 │\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10002 ┆ … ┆ DFL-CLU-0 ┆ #F4F5F0 ┆ 1. FC ┆ DFL-CLU-0 │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ 0000G ┆ ┆ Köln ┆ 00008 │\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10003 ┆ … ┆ DFL-CLU-0 ┆ #F4F5F0 ┆ 1. FC ┆ DFL-CLU-0 │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ 0000G ┆ ┆ Köln ┆ 00008 │\n", - "│ null ┆ null ┆ DFL-MAT-J ┆ 10004 ┆ … ┆ DFL-CLU-0 ┆ #F4F5F0 ┆ 1. FC ┆ DFL-CLU-0 │\n", - "│ ┆ ┆ 03WMX ┆ ┆ ┆ 0000G ┆ ┆ Köln ┆ 00008 │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10000 ┆ … ┆ DFL-CLU-0 ┆ #25282A ┆ FC Bayern ┆ DFL-CLU-0 │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ 00008 ┆ ┆ München ┆ 0000G │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10001 ┆ … ┆ DFL-CLU-0 ┆ #25282A ┆ FC Bayern ┆ DFL-CLU-0 │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ 00008 ┆ ┆ München ┆ 0000G │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10002 ┆ … ┆ DFL-CLU-0 ┆ #25282A ┆ FC Bayern ┆ DFL-CLU-0 │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ 00008 ┆ ┆ München ┆ 0000G │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10003 ┆ … ┆ DFL-CLU-0 ┆ #25282A ┆ FC Bayern ┆ DFL-CLU-0 │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ 00008 ┆ ┆ München ┆ 0000G │\n", + "│ null ┆ null ┆ DFL-MAT-J ┆ 10004 ┆ … ┆ DFL-CLU-0 ┆ #25282A ┆ FC Bayern ┆ DFL-CLU-0 │\n", + "│ ┆ ┆ 03WMX ┆ ┆ ┆ 00008 ┆ ┆ München ┆ 0000G │\n", "└───────────┴───────────┴───────────┴──────────┴───┴───────────┴───────────┴───────────┴───────────┘" ] }, @@ -307,47 +307,15 @@ "data": { "text/plain": [ "{'_entities': {'ball': {},\n", - " 'teams': {'away': {'players': {'DFL-OBJ-J015S4': {},\n", - " 'DFL-OBJ-J017RE': {},\n", - " 'DFL-OBJ-0000IA': {},\n", - " 'DFL-OBJ-0002DR': {},\n", - " 'DFL-OBJ-000191': {},\n", - " 'DFL-OBJ-0027G6': {},\n", - " 'DFL-OBJ-0000RP': {},\n", - " 'DFL-OBJ-J01B8N': {},\n", - " 'DFL-OBJ-0027G0': {},\n", - " 'DFL-OBJ-0002AU': {},\n", - " 'DFL-OBJ-002GCR': {},\n", - " 'DFL-OBJ-0026PM': {},\n", - " 'DFL-OBJ-0002F5': {},\n", - " 'DFL-OBJ-J01APO': {},\n", - " 'DFL-OBJ-J01N65': {},\n", - " 'DFL-OBJ-0027KL': {}},\n", - " 'jersey_colour': '#25282A',\n", - " 'name': 'FC Bayern München',\n", - " 'id': 'DFL-CLU-00000G'},\n", - " 'home': {'players': {'DFL-OBJ-0026ZI': {},\n", - " 'DFL-OBJ-002GBW': {},\n", - " 'DFL-OBJ-002GKO': {},\n", - " 'DFL-OBJ-0002BO': {},\n", - " 'DFL-OBJ-0027V2': {},\n", - " 'DFL-OBJ-0002HE': {},\n", - " 'DFL-OBJ-00012X': {},\n", - " 'DFL-OBJ-0027B6': {},\n", - " 'DFL-OBJ-002G4A': {},\n", - " 'DFL-OBJ-0027AX': {},\n", - " 'DFL-OBJ-J01DVC': {},\n", - " 'DFL-OBJ-0028BD': {},\n", - " 'DFL-OBJ-J01D1W': {},\n", - " 'DFL-OBJ-000270': {},\n", - " 'DFL-OBJ-J01BGM': {},\n", - " 'DFL-OBJ-002G89': {}},\n", + " 'teams': {'home': {'players': {},\n", " 'jersey_colour': '#F4F5F0',\n", " 'name': '1. FC Köln',\n", - " 'id': 'DFL-CLU-000008'}},\n", - " 'referees': {'referee:DFL-OBJ-00011J': {},\n", - " 'referee:DFL-OBJ-00011W': {},\n", - " 'referee:DFL-OBJ-000141': {}}}}" + " 'id': 'DFL-CLU-000008'},\n", + " 'away': {'players': {},\n", + " 'jersey_colour': '#25282A',\n", + " 'name': 'FC Bayern München',\n", + " 'id': 'DFL-CLU-00000G'}},\n", + " 'referees': {}}}" ] }, "execution_count": 7, @@ -366,27 +334,100 @@ "metadata": {}, "outputs": [ { - "ename": "FileNotFoundError", - "evalue": "No such file or directory (os error 2): ...rehouse/tracking/tracking_data/data/00000-0-2ce0e3ca-73c0-404e-9596-10570ff4a509.parquet (set POLARS_VERBOSE=1 to see full path)\n\nThis error occurred with the following context stack:\n\t[1] 'parquet scan'\n\t[2] 'sink'\n", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mFileNotFoundError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[8]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m start = time.perf_counter()\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m df = \u001b[43mpl\u001b[49m\u001b[43m.\u001b[49m\u001b[43mscan_parquet\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43midsse_warehouse/tracking/tracking_data/data/00000-0-2ce0e3ca-73c0-404e-9596-10570ff4a509.parquet\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcollect\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 4\u001b[39m query_time = time.perf_counter() - start\n\u001b[32m 5\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33m ✅ Loaded and pivoted \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mlen\u001b[39m(df)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m rows in \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mquery_time\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m.4f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33ms\u001b[39m\u001b[33m\"\u001b[39m)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/.venv/lib/python3.11/site-packages/polars/_utils/deprecation.py:97\u001b[39m, in \u001b[36mdeprecate_streaming_parameter..decorate..wrapper\u001b[39m\u001b[34m(*args, **kwargs)\u001b[39m\n\u001b[32m 93\u001b[39m kwargs[\u001b[33m\"\u001b[39m\u001b[33mengine\u001b[39m\u001b[33m\"\u001b[39m] = \u001b[33m\"\u001b[39m\u001b[33min-memory\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 95\u001b[39m \u001b[38;5;28;01mdel\u001b[39;00m kwargs[\u001b[33m\"\u001b[39m\u001b[33mstreaming\u001b[39m\u001b[33m\"\u001b[39m]\n\u001b[32m---> \u001b[39m\u001b[32m97\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunction\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/.venv/lib/python3.11/site-packages/polars/lazyframe/opt_flags.py:328\u001b[39m, in \u001b[36mforward_old_opt_flags..decorate..wrapper\u001b[39m\u001b[34m(*args, **kwargs)\u001b[39m\n\u001b[32m 325\u001b[39m optflags = cb(optflags, kwargs.pop(key)) \u001b[38;5;66;03m# type: ignore[no-untyped-call,unused-ignore]\u001b[39;00m\n\u001b[32m 327\u001b[39m kwargs[\u001b[33m\"\u001b[39m\u001b[33moptimizations\u001b[39m\u001b[33m\"\u001b[39m] = optflags\n\u001b[32m--> \u001b[39m\u001b[32m328\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunction\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/.venv/lib/python3.11/site-packages/polars/lazyframe/frame.py:2429\u001b[39m, in \u001b[36mLazyFrame.collect\u001b[39m\u001b[34m(self, type_coercion, predicate_pushdown, projection_pushdown, simplify_expression, slice_pushdown, comm_subplan_elim, comm_subexpr_elim, cluster_with_columns, collapse_joins, no_optimization, engine, background, optimizations, **_kwargs)\u001b[39m\n\u001b[32m 2427\u001b[39m \u001b[38;5;66;03m# Only for testing purposes\u001b[39;00m\n\u001b[32m 2428\u001b[39m callback = _kwargs.get(\u001b[33m\"\u001b[39m\u001b[33mpost_opt_callback\u001b[39m\u001b[33m\"\u001b[39m, callback)\n\u001b[32m-> \u001b[39m\u001b[32m2429\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m wrap_df(\u001b[43mldf\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcollect\u001b[49m\u001b[43m(\u001b[49m\u001b[43mengine\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcallback\u001b[49m\u001b[43m)\u001b[49m)\n", - "\u001b[31mFileNotFoundError\u001b[39m: No such file or directory (os error 2): ...rehouse/tracking/tracking_data/data/00000-0-2ce0e3ca-73c0-404e-9596-10570ff4a509.parquet (set POLARS_VERBOSE=1 to see full path)\n\nThis error occurred with the following context stack:\n\t[1] 'parquet scan'\n\t[2] 'sink'\n" + "data": { + "text/plain": [ + "'./idsse_warehouse'" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "warehouse_path" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "catalog.db \u001b[34mtracking\u001b[m\u001b[m\n" + ] + } + ], + "source": [ + "!ls idsse_warehouse/" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ✅ Loaded 11350189 rows in 0.3305s\n" ] + }, + { + "data": { + "text/html": [ + "
\n", + "shape: (5, 16)
competition_idseason_idmatch_idframe_idperiodtimestampentity_typeentity_idxyzball_statusball_poss_team_identity_keyaccvel
strstrstri64strdatetime[μs]strstrf32f32f32boolstrstrf64f64
nullnull"DFL-MAT-J03WN1"152866"other"2023-05-27 15:09:10.480"player""DFL-OBJ-00002Z"-1.83-34.169998nullfalse"DFL-CLU-00000B""DFL-OBJ-00002Z|home"0.00.0
nullnull"DFL-MAT-J03WN1"152867"other"2023-05-27 15:09:10.520"player""DFL-OBJ-00002Z"-1.89-34.07nullfalse"DFL-CLU-00000B""DFL-OBJ-00002Z|home"0.00.0
nullnull"DFL-MAT-J03WN1"152868"other"2023-05-27 15:09:10.560"player""DFL-OBJ-00002Z"-1.93-33.990002nullfalse"DFL-CLU-00000B""DFL-OBJ-00002Z|home"0.09.58
nullnull"DFL-MAT-J03WN1"152869"other"2023-05-27 15:09:10.600"player""DFL-OBJ-00002Z"-1.98-33.889999nullfalse"DFL-CLU-00000B""DFL-OBJ-00002Z|home"0.09.58
nullnull"DFL-MAT-J03WN1"152870"other"2023-05-27 15:09:10.640"player""DFL-OBJ-00002Z"-2.03-33.790001nullfalse"DFL-CLU-00000B""DFL-OBJ-00002Z|home"0.09.58
" + ], + "text/plain": [ + "shape: (5, 16)\n", + "┌──────────────┬───────────┬──────────────┬──────────┬───┬──────────────┬─────────────┬─────┬──────┐\n", + "│ competition_ ┆ season_id ┆ match_id ┆ frame_id ┆ … ┆ ball_poss_te ┆ entity_key ┆ acc ┆ vel │\n", + "│ id ┆ --- ┆ --- ┆ --- ┆ ┆ am_id ┆ --- ┆ --- ┆ --- │\n", + "│ --- ┆ str ┆ str ┆ i64 ┆ ┆ --- ┆ str ┆ f64 ┆ f64 │\n", + "│ str ┆ ┆ ┆ ┆ ┆ str ┆ ┆ ┆ │\n", + "╞══════════════╪═══════════╪══════════════╪══════════╪═══╪══════════════╪═════════════╪═════╪══════╡\n", + "│ null ┆ null ┆ DFL-MAT-J03W ┆ 152866 ┆ … ┆ DFL-CLU-0000 ┆ DFL-OBJ-000 ┆ 0.0 ┆ 0.0 │\n", + "│ ┆ ┆ N1 ┆ ┆ ┆ 0B ┆ 02Z|home ┆ ┆ │\n", + "│ null ┆ null ┆ DFL-MAT-J03W ┆ 152867 ┆ … ┆ DFL-CLU-0000 ┆ DFL-OBJ-000 ┆ 0.0 ┆ 0.0 │\n", + "│ ┆ ┆ N1 ┆ ┆ ┆ 0B ┆ 02Z|home ┆ ┆ │\n", + "│ null ┆ null ┆ DFL-MAT-J03W ┆ 152868 ┆ … ┆ DFL-CLU-0000 ┆ DFL-OBJ-000 ┆ 0.0 ┆ 9.58 │\n", + "│ ┆ ┆ N1 ┆ ┆ ┆ 0B ┆ 02Z|home ┆ ┆ │\n", + "│ null ┆ null ┆ DFL-MAT-J03W ┆ 152869 ┆ … ┆ DFL-CLU-0000 ┆ DFL-OBJ-000 ┆ 0.0 ┆ 9.58 │\n", + "│ ┆ ┆ N1 ┆ ┆ ┆ 0B ┆ 02Z|home ┆ ┆ │\n", + "│ null ┆ null ┆ DFL-MAT-J03W ┆ 152870 ┆ … ┆ DFL-CLU-0000 ┆ DFL-OBJ-000 ┆ 0.0 ┆ 9.58 │\n", + "│ ┆ ┆ N1 ┆ ┆ ┆ 0B ┆ 02Z|home ┆ ┆ │\n", + "└──────────────┴───────────┴──────────────┴──────────┴───┴──────────────┴─────────────┴─────┴──────┘" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ "start = time.perf_counter()\n", "\n", - "df = pl.scan_parquet(\"idsse_warehouse/tracking/tracking_data/data/00000-0-2ce0e3ca-73c0-404e-9596-10570ff4a509.parquet\").collect()\n", + "import glob\n", + "parquet_files = glob.glob(\n", + " os.path.join(warehouse_path, \"tracking\", \"tracking_data\", \"**\", \"*.parquet\"),\n", + " recursive=True,\n", + ")\n", + "\n", + "df = pl.scan_parquet(parquet_files).collect()\n", "query_time = time.perf_counter() - start\n", - "print(f\" ✅ Loaded and pivoted {len(df)} rows in {query_time:.4f}s\")\n", - "container.to_polars().head()" + "print(f\" ✅ Loaded {len(df)} rows in {query_time:.4f}s\")\n", + "df.head()" ] }, { @@ -398,14 +439,29 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Warehouse cleaned up.\n" + ] + } + ], "source": [ "if Path(warehouse_path).exists():\n", " shutil.rmtree(warehouse_path)\n", "print(\"Warehouse cleaned up.\")" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/examples/20260423-presentation-liss/build.sh b/examples/20260423-presentation-liss/build.sh new file mode 100755 index 0000000..85e7957 --- /dev/null +++ b/examples/20260423-presentation-liss/build.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# Build and serve the presentation +# This will generate presentation.slides.html and open it in your browser. + +jupyter nbconvert presentation.ipynb \ + --to slides \ + --RevealHelpTransformer.url_prefix=https://cdnjs.cloudflare.com/ajax/libs/reveal.js/4.1.2 \ + --RevealExporter.theme=simple \ + --RevealExporter.transition=slide \ + --post serve diff --git a/examples/20260423-presentation-liss/custom.css b/examples/20260423-presentation-liss/custom.css new file mode 100644 index 0000000..33fd174 --- /dev/null +++ b/examples/20260423-presentation-liss/custom.css @@ -0,0 +1,51 @@ +/* General Slide Styling */ +.reveal h1 { + font-size: 2.5em !important; + color: #1a73e8 !important; + text-transform: none !important; +} + +.reveal h2 { + font-size: 1.8em !important; + color: #444 !important; + border-bottom: 2px solid #1a73e8; + padding-bottom: 10px; + text-transform: none !important; +} + +.reveal p, .reveal li { + font-size: 1.2em !important; + line-height: 1.4em !important; +} + +/* Code Styling */ +.reveal pre code { + font-size: 1.1em !important; + line-height: 1.2em !important; + border-radius: 8px !important; + padding: 20px !important; +} + +.reveal .rendered_html table { + font-size: 0.8em !important; +} + +/* Layout Adjustments */ +.reveal .slides { + text-align: left !important; +} + +.reveal .progress { + color: #1a73e8 !important; +} + +/* RISE specific tweaks */ +.rise-enabled .reveal .slides { + font-size: 120%; /* Scale up everything for RISE */ +} + +/* Ensure images don't overflow */ +.reveal img { + max-height: 500px !important; + width: auto !important; +} diff --git a/examples/20260423-presentation-liss/img/building_bridges.jpeg b/examples/20260423-presentation-liss/img/building_bridges.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..866fc942875b90d76723a50a4b70c58c3f7bb007 GIT binary patch literal 371179 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 @XH3~dTNzi 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�g%U6;>bqT>zc(I|B%E)7yO5gX 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

@|!?qA@7rbGHAA~tJsn|Tp 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 literal 0 HcmV?d00001 diff --git a/examples/20260423-presentation-liss/img/pysport_logo.png b/examples/20260423-presentation-liss/img/pysport_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2987818fac454a96abeb0e3069b57d27fee5dba2 GIT binary patch literal 36396 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\n", + "\n", + "*(This cell links custom CSS for the presentation and is hidden during the show)*" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + }, + "tags": [] + }, + "source": [ + "# RFC: Tracking Data Container\n", + "### A Metadata-Aware High-Performance Container for Sports Tracking Data\n", + "\n", + "**Pieter Robberechts, Koen Vossen, et al.**\n", + "\n", + "April 2026\n", + "\n", + "\"logo\"\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + }, + "tags": [] + }, + "source": [ + "## The Fragmentation of Open-Source\n", + "\n", + "People are building great open source packages but there is little to no coordination and therefore no interoperability. \n", + "\n", + "\n", + "\n", + "See " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "jp-MarkdownHeadingCollapsed": true, + "slideshow": { + "slide_type": "slide" + }, + "tags": [] + }, + "source": [ + "Lots of time is wasted on transforming output from one package into input for another one. This can be solved by a common standard. This increases interoperability and saves you time.\n", + "\n", + "Sports analytics is currently suffering from **duplicated effort** across various open-source packages.\n", + "\n", + "- **Parsing & Modeling**: Kloppy, Floodlight, and others often build similar logic.\n", + "- **The Scalability Wall**: Object-based models (like Kloppy) are excellent for small datasets but fail when processing millions of rows of tracking data.\n", + "- **Inconsistent Schemas**: Every tool uses different column names, making interoperability a nightmare.\n", + "- Lots of time is wasted on transforming output from one package into input for another one. This can be solved by a common standard. \n", + "\n", + "- Lots of implemetation effort is wasted by re-implementing data parsers across various open-source packages. \n", + "\n", + "\n", + "| | databallpy | PreProcessing/Event | floodlight | gandula | soccercpd | defcon | ballradar | socceraction | soccer-xg | unravelsports | kloppy | ingestify |\n", + "|------------------|------------|---------------------|---------------|----------|-----------|----------|-----------|--------------|-----------|---------------|----------|-----------|\n", + "| Ingestion | | | | | | | | | | | | ✔️ |\n", + "| Data management | | | | | | | | | | | | ✔️ |\n", + "| Parsing Events | ✔️ | ✔️ | ✔️ | ✔️ | | ✔️ | ✔️ | ✔️ | ✔️ | | ✔️ | |\n", + "| Parsing Tracking | ✔️ | ✔️ | ✔️ | ✔️ | | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | |\n", + "| Event Sync | ✔️ | | | | | ✔️ | | | | | | |\n", + "| Metrics | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | |\n", + "| Visuals | ✔️ | | | ✔️ | | | ✔️ | ✔️ | | | | |\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + }, + "tags": [] + }, + "source": [ + "## 🌉🌁 𝐁𝐮𝐢𝐥𝐝𝐢𝐧𝐠 𝐛𝐫𝐢𝐝𝐠𝐞𝐬! \n", + "\n", + "We brought together contributors from different open source projects to discuss how we can align our work and improve interoperability in football analytics. \n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "subslide" + }, + "tags": [] + }, + "source": [ + "Socceraction v1.5.0 added support for loading data using kloppy." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/probberechts/Developer/tracking_data_container/.venv/lib/python3.11/site-packages/kloppy/_providers/statsbomb.py:86: UserWarning: \n", + "\n", + "You are about to use StatsBomb public data.\n", + "By using this data, you are agreeing to the user agreement. \n", + "The user agreement can be found here: https://github.com/statsbomb/open-data/blob/master/LICENSE.pdf\n", + "\n", + " warnings.warn(\n" + ] + }, + { + "ename": "AttributeError", + "evalue": "FIXED_HOME_AWAY", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mAttributeError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[1]\u001b[39m\u001b[32m, line 5\u001b[39m\n\u001b[32m 2\u001b[39m dataset = statsbomb.load_open_data(match_id=\u001b[32m8657\u001b[39m)\n\u001b[32m 4\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msocceraction\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mspadl\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mkloppy\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m convert_to_actions\n\u001b[32m----> \u001b[39m\u001b[32m5\u001b[39m spadl_actions = \u001b[43mconvert_to_actions\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdataset\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mgame_id\u001b[49m\u001b[43m=\u001b[49m\u001b[32;43m8657\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[32m 7\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msocceraction\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01matomic\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mspadl\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mas\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01matomicspadl\u001b[39;00m\n\u001b[32m 8\u001b[39m atomic_spadl_actions = atomicspadl.convert_to_atomic(spadl_actions)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Developer/tracking_data_container/.venv/lib/python3.11/site-packages/socceraction/spadl/kloppy.py:90\u001b[39m, in \u001b[36mconvert_to_actions\u001b[39m\u001b[34m(dataset, game_id)\u001b[39m\n\u001b[32m 82\u001b[39m warnings.warn(\n\u001b[32m 83\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mConverting \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mdataset.metadata.provider\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m data is only supported from \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 84\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mKloppy version \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m_SUPPORTED_PROVIDERS[dataset.metadata.provider]\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m (you have \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m_KLOPPY_VERSION\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m). \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 85\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mThe result may be incorrect or incomplete.\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 86\u001b[39m )\n\u001b[32m 88\u001b[39m \u001b[38;5;66;03m# Convert the dataset to the SPADL coordinate system\u001b[39;00m\n\u001b[32m 89\u001b[39m new_dataset = dataset.transform(\n\u001b[32m---> \u001b[39m\u001b[32m90\u001b[39m to_orientation=\u001b[43mOrientation\u001b[49m\u001b[43m.\u001b[49m\u001b[43mFIXED_HOME_AWAY\u001b[49m, \u001b[38;5;66;03m# FIXME\u001b[39;00m\n\u001b[32m 91\u001b[39m to_coordinate_system=_SoccerActionCoordinateSystem(normalized=\u001b[38;5;28;01mFalse\u001b[39;00m),\n\u001b[32m 92\u001b[39m )\n\u001b[32m 94\u001b[39m \u001b[38;5;66;03m# Convert the events to SPADL actions\u001b[39;00m\n\u001b[32m 95\u001b[39m actions = []\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/.local/share/uv/python/cpython-3.11.14-macos-aarch64-none/lib/python3.11/enum.py:786\u001b[39m, in \u001b[36mEnumType.__getattr__\u001b[39m\u001b[34m(cls, name)\u001b[39m\n\u001b[32m 784\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mcls\u001b[39m._member_map_[name]\n\u001b[32m 785\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m786\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mAttributeError\u001b[39;00m(name) \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m\n", + "\u001b[31mAttributeError\u001b[39m: FIXED_HOME_AWAY" + ] + } + ], + "source": [ + "from kloppy import statsbomb\n", + "dataset = statsbomb.load_open_data(match_id=8657)\n", + "\n", + "from socceraction.spadl.kloppy import convert_to_actions\n", + "spadl_actions = convert_to_actions(dataset, game_id=8657)\n", + "\n", + "import socceraction.atomic.spadl as atomicspadl\n", + "atomic_spadl_actions = atomicspadl.convert_to_atomic(spadl_actions)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "subslide" + }, + "tags": [] + }, + "source": [ + "Databallpy added support for loading data using kloppy." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Succesfully loaded databallpy.game.Game object: Fortuna Düsseldorf 0 - 1 1. FC Nürnberg\n" + ] + } + ], + "source": [ + "# create a Kloppy TrackingDataset and/or EventDataset\n", + "from kloppy import sportec\n", + "\n", + "event_dataset = sportec.load_open_event_data(match_id=\"J03WPY\")\n", + "tracking_dataset = sportec.load_open_tracking_data(match_id=\"J03WPY\", only_alive=False)\n", + "\n", + "# transform the kloppy dataset to a databallpy game object:\n", + "from databallpy import get_game_from_kloppy\n", + "\n", + "game = get_game_from_kloppy(tracking_dataset=tracking_dataset,event_dataset=event_dataset)\n", + "print(f\"Succesfully loaded {game}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + }, + "tags": [] + }, + "source": [ + "## Is a Dataframe Agreement Enough?\n", + "\n", + "A common suggestion is: *\"Why don't we just all use Polars and agree on column names?\"*\n", + "\n", + "We believe this is **insufficient** because:\n", + "- **Metadata Loss**: Dataframes lose the context of coordinate systems, field sizes, and team orientations.\n", + "- **Semantic Fragility**: Column names like `x_1` or `home_7_y` are fragile and lack domain-specific logic.\n", + "- **Logic Duplication**: Operations like coordinate normalization shouldn't be re-implemented for every project." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + }, + "tags": [] + }, + "source": [ + "## The Proposed Solution: TDC\n", + "\n", + "The **Tracking Data Container** is a shared, standardized, metadata-aware interface.\n", + "\n", + "- **Implemented in Rust**: Fast.\n", + "- **Backed by Apache Arrow**: Zero-copy, memory-efficient.\n", + "- **(Soft) Enforced Metadata**: The container *knows* what it's holding.\n", + "- **Shared CDF Schema**: Guaranteed interoperability across the ecosystem." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + }, + "tags": [] + }, + "source": [ + "## Why Rust, Arrow & CDF?\n", + "\n", + "### Apache Arrow\n", + "- A community-driven standard for columnar in-memory data.\n", + "- Enables **zero-copy** transfer between Rust and Python." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + }, + "tags": [] + }, + "source": [ + "### Common Data Format (CDF)\n", + "- We align with the **CDF v0.2.2** standard for hierarchical naming.\n", + "- `ball/x`, `teams/{tid}/players/{pid}/y`\n", + "- This ensures compatibility with research and federation-level tooling." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + }, + "tags": [] + }, + "source": [ + "## 5. Design & Functionalities\n", + "\n", + "- **TrackingDataContainer**: The eager interface for interactive work.\n", + "- **DataField**: Domain-specific representation of a column.\n", + "- **ColumnSelector**: Metadata-aware querying (e.g., `selector.players('home').x`).\n", + "- **CoordinateSystem**: Built-in support for transformations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "import tracking_data_container as tdc\n", + "from pathlib import Path\n", + "\n", + "# Load and transform in 3 lines\n", + "container = tdc.load_sportec(\"../../data/sportec/tracking.xml\")\n", + "target_cs = tdc.CoordinateSystem(x_range=(-52.5, 52.5), y_range=(-34, 34), origin=\"center\")\n", + "container = container.transform_coordinates(target_cs)\n", + "\n", + "# Metadata-aware selection\n", + "home_x = container.select(container.selector.players(team_id=\"home\").attributes([\"x\"]))\n", + "home_x.to_polars().head()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## 6. Advanced: Lazy Eval & Filter Pushdown\n", + "\n", + "For high-cardinality data like **Pose Tracking (Skeletal)**, eager loading is impossible.\n", + "\n", + "- **LazyTrackingDataContainer**: Uses DataFusion logical plans.\n", + "- **Filter Pushdown**: Queries like `filter(period == 1)` skip reading irrelevant data from disk.\n", + "- **Performance**: Scales to hundreds of joints across multiple players." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "# Lazy load Hawkeye skeletal data\n", + "H_DIR = Path(\"../../data/hawkeye\")\n", + "hawkeye = tdc.load_hawkeye(\n", + " list(H_DIR.glob(\"*.ball.json\"))[:5], \n", + " list(H_DIR.glob(\"*.centroids.json\"))[:5], \n", + " joint_paths=list(H_DIR.glob(\"*.joints.json\"))[:5], \n", + " meta_path=H_DIR / \"meta.json\"\n", + ").lazy()\n", + "\n", + "# Pushdown: Only process what we need\n", + "subset = hawkeye.filter(tdc.col(\"period\") == 1).select([\"timestamp\", \"ball/x\", \"ball/y\"])\n", + "subset.collect().to_polars().head()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## 7. Warehouse Scale: Apache Iceberg\n", + "\n", + "Storage is optimized via **Apache Iceberg** and a **Semi-Long Format**.\n", + "\n", + "### Why Iceberg?\n", + "- **Partition Pruning**: Skip entire matches during scan.\n", + "- **Bucket Pruning**: Read only specific players from a match file.\n", + "- **Schema Evolution**: Add new metrics/columns without rewriting the entire warehouse.\n", + "- **99% Pruning Efficiency**: Read 1MB of data for a specific player query vs. 100MB+ for the full match." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## 8. Get Involved & Discussion\n", + "\n", + "TDC is **open-source** and built on community collaboration.\n", + "\n", + "### Open Questions for Discussion:\n", + "- Should we support nested structs for joint data, or stick to flat hierarchical names?\n", + "- How do we best integrate event data (which doesn't fit the Arrow table model perfectly)?\n", + "- What are the priority formats for the next batch of loaders?\n", + "\n", + "**GitHub**: `PySport/tracking-data-container` \n", + "**RFC**: Join the conversation on the RFC document!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.14" + }, + "livereveal": { + "scroll": true, + "theme": "simple", + "transition": "slide" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/20260423-presentation-liss/presentation.slides.html b/examples/20260423-presentation-liss/presentation.slides.html new file mode 100644 index 0000000..35a3cc1 --- /dev/null +++ b/examples/20260423-presentation-liss/presentation.slides.html @@ -0,0 +1,7740 @@ + + + + + + + +presentation slides + + + + + + + + + + + + + + + + + +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + diff --git a/examples/idsse_warehouse/catalog.db b/examples/idsse_warehouse/catalog.db deleted file mode 100644 index 0d807f53775e89c3908b6fc56b90ad562323c55b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20480 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 diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/748d49d5-48bd-40a1-aac5-ac3df2728952-m0.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/748d49d5-48bd-40a1-aac5-ac3df2728952-m0.avro deleted file mode 100644 index 8f485b4390f4391188ee81efa5e3affeabd858ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4508 zcmb`KTWl0n7{{S4cw4Ivq`VMwG9k)kmd>TyZR?8Y|=ElXXpIC zZ+_q9d}pwHaD8jW*GEVd|JQOvpc5_;dIX<(fldWcsF=hue4%IPh+rB*k1Pr{uCgbp znCc@Mtci&|racVbJ#r{8Jpzw|3YiqctxCLAcd9O-CN&*@c>?SjCei}Xi_%eG0)khI|k8P4N|c%C9S$+Q?T+R^Rtcr9{TXX%Tb6BVAdTE z_@g*T2%;2*2SEfe4gC05?mUv<;V{BTFl{PKiYT$e#;!l&P#%+c(Qunw+cqTFfn_mS z%Yk5*-~;%K1B(hhi2an7giIi|PBc5HmWfyAjk;FZifIweUsAhct=c>@n1MQHm73s6 zLogr2Iw4t2_{O8a7&lW2(YA# z%D|H)g}qKhcyCmqhy@zaz_o}NzBWc>>=Oe45;{T{>#&#+E9Nwku=F(D3808TJn91w zLs?3sbyy@d>jS(DkW)dSf{)D57-O6^!KwS^VWOF~R-T}wzLsSvnMb@DYlika6kCXC zAg000gc_;x)Ez6R+8TRep?Vjn&G(A9=xdpeJr7T?S2roCLi>Y05;c}uUy6b-D$99o zOqRQ1R%}P2S&b5|6)CXR1ky>Waz5qx7`6glq0yG;{Zo6oCQB^zmNRVX*xJvJHUuY^ zD@>MPJ>9t2LRf|U*kvIK=^)cU)PX++l(U5VFid|m9WL%rm=Ft5c7yi3qYXgW4JZ{U z2DEyV-K$5bNR39xYe$5{)UCjPzQGRF8-ydZ~Jhf29)V|z)MYQB-#s&)ZPwR`O_)vlEHD=K`oyOA*2#K8ttN|j*@R2Th0n`R%wWV z>pmJ{Lk}i}byM{*x~Z~EH(eB>+Q4?oSuF+;4pl9>>%>ViKhUd*!7c8XLg;G=lJrqW z<7>9=O}Kn)1bGz6qq60c(;@U-%cPo9srZC0CLg@{rAM@n-{UQn)6-aOdx;gYJ01+%uqf*v&lMeL5C9CHVzpsfZU|g;~}0 zh|ey)a7_DP0S77XQ2X#?;sJ*oiYo zzc~8aiJs<T#f{P@@2vk0!&jv8AE7Zx8eR!J(B+cyP~6l diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/eab6cbfa-fef9-4779-b2e1-c63290db081c-m0.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/eab6cbfa-fef9-4779-b2e1-c63290db081c-m0.avro deleted file mode 100644 index 5b886573fc418d457dc289087f4f0e56b80696f1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4507 zcmb`KPfQ$D9LEbZjaEbt1TQ_jOo+B+w#*;?d-0%6cVVHn6%%nYc{}qKrp(SvXWl@t zau6=0G5%}PUOdv`rHQE;FJ446q7>VPvG9^#Yv=qEIr4W%xqh$XUTOgg#jmY+Pnf zlrh!EG*}Z8drW&2zI)_MV0r`|2Ng0chFg_*tL~ItLQQHq_QM29*z#iqHCFHm_IJ3s zBi>vl6dRag?KEgS-2H+mgm^LxfFuG3Vp9{qco|Ly))}C}^Z^%p;cI1lm*{&(3PO4X zo~)M+lpGJ>WhRK{1A%&_8Bci55#2F}-ei!Hg(+#$9h-uc$C#fj{P)m@cb<AXW*`?4mF~5DR19EGg`DBErX`5=AV~hz71j#PGE-Dq^1)2$0Y*!dQpJj94+Jk%XnE=}rJe1maO2 zfEdbBBCW$BsaPN6Wq_O#3Ke{0hQ=7O0G`PlRD6nk}(k}|YE=p#{Msr02N2;;Kc ztxd>sPt1z#NHnW)!nGm=)|x;%NmcGnc|L)yfLCa=C3^qVo}TFf3%%uxnmV>l@uLmF z$>j>u1z1lvF18R>VII3IL?Iny8i+dZ$AEH{kROKWkEg@M9SRd-Bg$UTUU#$xD0=~= zBE^7qj>C>5#JD0%IOkeIp^7|=KP;eoJipg}yCX3LHhZ7aZXqR~A%U`1wtmE8kY zDn6Yrl*e5T|2g%72>SY z5ChkJFvOZ3ObY9!>LYYhTuLVfb z2OW(s*}6C7^05)*Q6!IwmQzfJ(0479YEG%-6S|=g)Wj?oo3Bg|@o+&bQif|#^tmhHNEJi=udi+{@t|J?Y(S!jPvm81{#-1jTl&u-qn{ZwW!bNE*K!S9B0 zxlAsqG=zPb2Z%catti!BR_oxk0E_Co9E(%s7!T4z4}efIH&r>}Qj&*uk}{=r=TTE0L3 z$y%q}{rOs6$#t&fyVS0$9mlfD$I`&9wqxR9F`xPQ3Ab^7WAG&IYyNuaOFH}hrI{-; zzntIS@ZMKTi%a|FUl`aYoqwzDTZ}(AbLOub_kzY7_Y|q={{5ehe>8k<=vv#dxIB;< YzFPn1+{^Qg9gT+2u%~V0^=cyj177c>r~m)} diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-2148665455330551612-0-eab6cbfa-fef9-4779-b2e1-c63290db081c.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-2148665455330551612-0-eab6cbfa-fef9-4779-b2e1-c63290db081c.avro deleted file mode 100644 index 6d2c4aaa48421ea2e625fd10d239cd075c8e2801..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1947 zcmb7EX-E`d7*5FwitaWGk!>mo+RfdancY>2jvrBBbW33}e9nB^PR-TKx0%>hY(+|i z-3JDRQFhuP7(!A)Wp@;0K_SxdqZT2{=%VAB)!9ZzxBB~i@AEwG`@F}zf|W@FY?=fV zEhr-=&t-Vrl;7*6DViop7frjpZqGOcs8B-APGLSD<#PE*kH*_aoe1zy;57V}S4>DijQxs0;sa1ai~>+F9)$D9VYJJIQ3j8OR9;%4hj22h z6X(b*KsX!wl7+MFg**z{XD+hG^dS*;*t^jXptPbJ6}L%6kg-N5H1-Vfx(=f$kY#XY zMkz*?9uuCIP-i$RO9*@373B6M0vQcp+8ba2(DCs+D@eeQ*g~U%1O#RettrIkR_i)GF4OGg5mHDHaKxX51*!^)bPV=T)Xzo7GWzio zkD#b;@So_!>#a{e;3W-@VN9j{1{jQ4rXeVhWdQ=ox;*W0{DXsI7o-F2*7NB&4)H1o zgC-w;;==?&0>bI)>YbKakPD&86yVXcth3v6bWFTO2x`P%SVM}^Bdh7i`~#~lWSmHq zcKL~L_%GV4U=bri1UP_z#VV80|0y27&-x}2;a=y?<{z$(^sgUQ>Q8z6a?klbM|TsAH#FZe3_P`XaNuZaFBc70T1^ z2A;j`7p)@h&VkN1TZ1QqgB&~h=CYaFuAE5!RV6q!6T4=N4L(k*px)S%KTOX6*QSiW za__+W;;5f&c~bK#r}9YU#{<=6TN~y+UH!9q>4>I5#~T(oH!hA0UXYetHe^gIJh<~= z>9)0{SJU3_x&1l%>G_YAuMZ!*YvuQzDmjx{ku#xWa$C}Zn;SigvuADE-cUbu1KFJa Y`A1Y*zpk}y-6ik44N1F?-R^49Z+AeP`Tzg` diff --git a/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-2774007322244479359-0-13c097fb-ed6a-4e53-95aa-61c36e19e9b5.avro b/examples/idsse_warehouse/tracking/match_metadata/metadata/snap-2774007322244479359-0-13c097fb-ed6a-4e53-95aa-61c36e19e9b5.avro deleted file mode 100644 index f90f7b47ea93cf3ef3ea10fb7d384116a27e90ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1902 zcmb7ETSyd97_QCkT4H8G64MaU&6u6tb#}Et2qnW5jb7SfJkFfWsky8(XIED-NRYyy zDC)@+qNoTGONtE2jZEnw=^oaL(G|;}#3G~1nbp}wM^}42=l{O%|G)oIRJM0!e2OLm zzgDiG41rIl8OBLbjDx0Wr_;%J94^l)KTx5JGR$Evmm||nQMAWFGj@lYVKOsUXs{vx zWe#S@0jUhCHag9xs1iUKL8xkiBHQR>5LA^7oWetH5f6PLKrkipq;cg?0xipGNEGrz zGK(Np$SEpMt|v&J07YKIZ{8YG22$i#0QoTLD| zx#`vxL@S8F5Zgkdf*1s553MOg=T;j(K9p(pyrdiuMI7;OVS%cGkdDC~O8U7NT}C&4 z5)hR14gMD$d%gAP2ZF5OF^s5;+yH~oV;X`oMG+y8t;;h4$3HkYazRGXc0Hfb;}ETa zG-!6?Pkfj_KtwoQU46__i%KO_*)lwumUXt9j*f{pgrG)F!5Z-UCuKDqnQ5@bygSv!MTbG4{>>DN=Or#+z)w~H^wbh;D<7s;@QwA=-ol)NZd-yg+39_7C*eY$%^7Y< zY_~m1K65N@_*%u|A=H`btyY`2^{mZ3f|hU0KIa+=lHGNiZ}xY!y(kUud}%LbJ_m1f zg8qALJyk_Rv$OUNw4eVGo)drL(%c2tJFc{!Z7<||R}2$x_-Kw4lS$T*-Wo$D09kWII?AXkA6gS~}j z1vUhvht?EwbE~f&ADWoG7RuOo9I?ExK+{0Nz+ew0<341UF@T>01SMmGm!s|1+n9bJ z$T}Xwv`Xd%n2bSU2qKCiLLgh0Cj-YnI5>SlvS`N0Cwm-nRZu2PEB?fX3B*N&(>2sr zEVZa~LXC~!(X^~HLmVA~H-Vr|ZNM5=)pc2kBeMxsL&!XlnwJhxsniDA7hr;wAObu< zz+x3)^nZ%y?z6E;RBFq@d~bMeKpm{z85~{ubn}j5(ou7bDZE4%T^;^2*f5;0`ew)P z9i8c^YOJ5>ogC^L|Jd|=tShhPN{PB>f8C3(ON-x=kL&#-`Q6tK&-Lq1E8gCEbh&5m z?+?>A%JO@(2cM;W`rHAjH2iF$Ayn0RqI~36UvFOZ!};0w`?_B`f4mBhv)jMbof>eu4BajWjfR`=zd76Y`pdh4=_ARO&@S%h)ZdF^$Jo0Cr3+If;^Ohung0U{ CD`4pW diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-eb9e2be6-0ccc-4e61-aa83-a184ad41f6f1.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-eb9e2be6-0ccc-4e61-aa83-a184ad41f6f1.metadata.json deleted file mode 100644 index c4a2b91..0000000 --- a/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-eb9e2be6-0ccc-4e61-aa83-a184ad41f6f1.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"fff6c07b-7d52-4453-853f-06f13b09d84d","last-updated-ms":1775658762742,"last-column-id":12,"schemas":[{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":true},{"id":4,"name":"frame_id","type":"long","required":true},{"id":5,"name":"period","type":"string","required":true},{"id":6,"name":"timestamp","type":"timestamp","required":true},{"id":7,"name":"entity_type","type":"string","required":true},{"id":8,"name":"entity_id","type":"string","required":true},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]}],"current-schema-id":0,"partition-specs":[{"spec-id":0,"fields":[{"source-id":3,"field-id":1000,"transform":"identity","name":"match_id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"snapshots":[],"snapshot-log":[],"metadata-log":[],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":0} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-bf153c68-f126-4491-bfa6-00cf45343e37.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-bf153c68-f126-4491-bfa6-00cf45343e37.metadata.json deleted file mode 100644 index 683aaca..0000000 --- a/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-bf153c68-f126-4491-bfa6-00cf45343e37.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"fff6c07b-7d52-4453-853f-06f13b09d84d","last-updated-ms":1775658790066,"last-column-id":15,"schemas":[{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":true},{"id":4,"name":"frame_id","type":"long","required":true},{"id":5,"name":"period","type":"string","required":true},{"id":6,"name":"timestamp","type":"timestamp","required":true},{"id":7,"name":"entity_type","type":"string","required":true},{"id":8,"name":"entity_id","type":"string","required":true},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":false},{"id":4,"name":"frame_id","type":"long","required":false},{"id":5,"name":"period","type":"string","required":false},{"id":6,"name":"timestamp","type":"timestamp","required":false},{"id":7,"name":"entity_type","type":"string","required":false},{"id":8,"name":"entity_id","type":"string","required":false},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false},{"id":13,"name":"vel","type":"double","required":false},{"id":14,"name":"acc","type":"double","required":false},{"id":15,"name":"entity_key","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":3,"field-id":1000,"transform":"identity","name":"match_id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":3805096703135495509,"snapshots":[{"snapshot-id":3805096703135495509,"sequence-number":1,"timestamp-ms":1775658790066,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-3805096703135495509-0-3aa2fe75-8006-42c8-97d9-5e4835aa023e.avro","summary":{"operation":"append","added-files-size":"42344336","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"42344336","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":3805096703135495509,"timestamp-ms":1775658790066}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-eb9e2be6-0ccc-4e61-aa83-a184ad41f6f1.metadata.json","timestamp-ms":1775658762742}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":3805096703135495509,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":1} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-d345f9cd-ab81-46c9-92b2-74595885722c.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-d345f9cd-ab81-46c9-92b2-74595885722c.metadata.json deleted file mode 100644 index 87e9084..0000000 --- a/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-d345f9cd-ab81-46c9-92b2-74595885722c.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"fff6c07b-7d52-4453-853f-06f13b09d84d","last-updated-ms":1775658816184,"last-column-id":15,"schemas":[{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":true},{"id":4,"name":"frame_id","type":"long","required":true},{"id":5,"name":"period","type":"string","required":true},{"id":6,"name":"timestamp","type":"timestamp","required":true},{"id":7,"name":"entity_type","type":"string","required":true},{"id":8,"name":"entity_id","type":"string","required":true},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":false},{"id":4,"name":"frame_id","type":"long","required":false},{"id":5,"name":"period","type":"string","required":false},{"id":6,"name":"timestamp","type":"timestamp","required":false},{"id":7,"name":"entity_type","type":"string","required":false},{"id":8,"name":"entity_id","type":"string","required":false},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false},{"id":13,"name":"vel","type":"double","required":false},{"id":14,"name":"acc","type":"double","required":false},{"id":15,"name":"entity_key","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":3,"field-id":1000,"transform":"identity","name":"match_id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":6133585602095476169,"snapshots":[{"snapshot-id":3805096703135495509,"sequence-number":1,"timestamp-ms":1775658790066,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-3805096703135495509-0-3aa2fe75-8006-42c8-97d9-5e4835aa023e.avro","summary":{"operation":"append","added-files-size":"42344336","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"42344336","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":6133585602095476169,"parent-snapshot-id":3805096703135495509,"sequence-number":2,"timestamp-ms":1775658816184,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-6133585602095476169-0-b2eff7a4-107d-40c3-846c-6d69ba0f72ff.avro","summary":{"operation":"append","added-files-size":"39125719","added-data-files":"2","added-records":"3832763","changed-partition-count":"1","total-data-files":"4","total-delete-files":"0","total-records":"7919839","total-files-size":"81470055","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":3805096703135495509,"timestamp-ms":1775658790066},{"snapshot-id":6133585602095476169,"timestamp-ms":1775658816184}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-eb9e2be6-0ccc-4e61-aa83-a184ad41f6f1.metadata.json","timestamp-ms":1775658762742},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-bf153c68-f126-4491-bfa6-00cf45343e37.metadata.json","timestamp-ms":1775658790066}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":6133585602095476169,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":2} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-97d8ffab-78aa-49d0-8fa1-f6314d7b0ee9.metadata.json b/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-97d8ffab-78aa-49d0-8fa1-f6314d7b0ee9.metadata.json deleted file mode 100644 index f469351..0000000 --- a/examples/idsse_warehouse/tracking/tracking_data/metadata/00003-97d8ffab-78aa-49d0-8fa1-f6314d7b0ee9.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"location":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data","table-uuid":"fff6c07b-7d52-4453-853f-06f13b09d84d","last-updated-ms":1775658839577,"last-column-id":15,"schemas":[{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":true},{"id":4,"name":"frame_id","type":"long","required":true},{"id":5,"name":"period","type":"string","required":true},{"id":6,"name":"timestamp","type":"timestamp","required":true},{"id":7,"name":"entity_type","type":"string","required":true},{"id":8,"name":"entity_id","type":"string","required":true},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false}],"schema-id":0,"identifier-field-ids":[]},{"type":"struct","fields":[{"id":1,"name":"competition_id","type":"string","required":false},{"id":2,"name":"season_id","type":"string","required":false},{"id":3,"name":"match_id","type":"string","required":false},{"id":4,"name":"frame_id","type":"long","required":false},{"id":5,"name":"period","type":"string","required":false},{"id":6,"name":"timestamp","type":"timestamp","required":false},{"id":7,"name":"entity_type","type":"string","required":false},{"id":8,"name":"entity_id","type":"string","required":false},{"id":9,"name":"team_id","type":"string","required":false},{"id":10,"name":"x","type":"float","required":false},{"id":11,"name":"y","type":"float","required":false},{"id":12,"name":"z","type":"float","required":false},{"id":13,"name":"vel","type":"double","required":false},{"id":14,"name":"acc","type":"double","required":false},{"id":15,"name":"entity_key","type":"string","required":false}],"schema-id":1,"identifier-field-ids":[]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[{"source-id":3,"field-id":1000,"transform":"identity","name":"match_id"}]}],"default-spec-id":0,"last-partition-id":1000,"properties":{},"current-snapshot-id":6127150891889527239,"snapshots":[{"snapshot-id":3805096703135495509,"sequence-number":1,"timestamp-ms":1775658790066,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-3805096703135495509-0-3aa2fe75-8006-42c8-97d9-5e4835aa023e.avro","summary":{"operation":"append","added-files-size":"42344336","added-data-files":"2","added-records":"4087076","changed-partition-count":"1","total-data-files":"2","total-delete-files":"0","total-records":"4087076","total-files-size":"42344336","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":6133585602095476169,"parent-snapshot-id":3805096703135495509,"sequence-number":2,"timestamp-ms":1775658816184,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-6133585602095476169-0-b2eff7a4-107d-40c3-846c-6d69ba0f72ff.avro","summary":{"operation":"append","added-files-size":"39125719","added-data-files":"2","added-records":"3832763","changed-partition-count":"1","total-data-files":"4","total-delete-files":"0","total-records":"7919839","total-files-size":"81470055","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1},{"snapshot-id":6127150891889527239,"parent-snapshot-id":6133585602095476169,"sequence-number":3,"timestamp-ms":1775658839577,"manifest-list":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-6127150891889527239-0-236c935e-8816-455f-987e-f46e735afb70.avro","summary":{"operation":"append","added-files-size":"35081294","added-data-files":"1","added-records":"3430350","changed-partition-count":"1","total-data-files":"5","total-delete-files":"0","total-records":"11350189","total-files-size":"116551349","total-position-deletes":"0","total-equality-deletes":"0"},"schema-id":1}],"snapshot-log":[{"snapshot-id":3805096703135495509,"timestamp-ms":1775658790066},{"snapshot-id":6133585602095476169,"timestamp-ms":1775658816184},{"snapshot-id":6127150891889527239,"timestamp-ms":1775658839577}],"metadata-log":[{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00000-eb9e2be6-0ccc-4e61-aa83-a184ad41f6f1.metadata.json","timestamp-ms":1775658762742},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00001-bf153c68-f126-4491-bfa6-00cf45343e37.metadata.json","timestamp-ms":1775658790066},{"metadata-file":"file:///Users/probberechts/Developer/tracking_data_container/examples/idsse_warehouse/tracking/tracking_data/metadata/00002-d345f9cd-ab81-46c9-92b2-74595885722c.metadata.json","timestamp-ms":1775658816184}],"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0,"refs":{"main":{"snapshot-id":6127150891889527239,"type":"branch"}},"statistics":[],"partition-statistics":[],"format-version":2,"last-sequence-number":3} diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/236c935e-8816-455f-987e-f46e735afb70-m0.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/236c935e-8816-455f-987e-f46e735afb70-m0.avro deleted file mode 100644 index 8342db8231c337a58eb919b18ccf99bf3673bba6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5590 zcmb_geQ*@z8BZ`wqnt6&7?6p>>w;md7kj&x%Uwce42VP~1e^prDsx`n+kG$DvS02; zAm~}H(2@{PL4r=znd&5UM)<>0L>+ZNhX^gGjG#_CR#Y$u7;2q%rqdSe^X}&MZZF9- zi8qtUBzyaOyuau9{hsH&Rq>w}m-!67gUI-svWFP6OCwAJW0<*PU8)Cm|Ruw;;=-|@h?9t?twNg4-T zV;P%#$D{(+y0!@&j^MGC)O;z2TuY7UqB@Dg*149ZC=8Fc@vU~X-kcBRyNaHT@yFTw9|%(8)e$4 zlSmmeLABx%$&+EGg)q-g!Yfs%TS%}aPrsToaH zX;^o-SYv2}-&G8BHB?LIiERq!O}`Hm4g{b)rj8XOq3SYF*>P*qlt#$yvMU*7ysp9D zv_KU##AZz=(GB4LG_C@7>gsL0DiFR*APEVZq>NV;6Vfdeu!$?}&z-~U|4CY95VQiX z>cA)D>I{O+og@&55H>Bt{nS1R3c$;bNFhd4OeX0^Sr!V`j1JYbyLH5=W5#C8)-Z}C zLD>;w(>@A+K+f2bOr`u6e6hoB<{Fr4B7dicbXdxRAt-T+qI!}KgXeR(Pr*1YH8a6v} z13{~1%AF>P8*7EiV>*D)J*~^pI??Pv%i56LWyISTwtpw7Q4z8_gN{90*xU$HI*rSi zQL&~_0hfvZM25u3X0Z!Rrlm(tFh*0;M`>p1Y=EN(rHgiVT0Ri%9b{UXMEnkt7{#$c z1OzCX9fX$85gJ5=#*4DU<`}P9z{}ipvzX%WE)#vF%`BuQkmJEXq7#gk%^Silq#vxc~!DxB}U&E?L7i%kI0V;dWK9` z*D=T_D3eKKkYvTVe~idtmppFJhqaA}@pHvt>=WZsD-%O-s~ zX!`sfn@BQBOE0PIB9Eoj_QHo48EZ^3N{iyrLi)fVb_>>Mi5zs0C?HdX9}1|ACG>|N z8f(4fvJ-L|VlKY1v*K%t?ux#Ne%70wwIwpw00?Mh4)mR|H zP>spN#B|Ydn`GpU5X{;I-RaIaxkjN$#~Y1zD!ksM*_$Z1M``ycE~#;E2@OpW&6t`< z7{nY!gkaWs=!Va15bkn8BqGDjDxEaltkV02vs&ey<83qr6T?jx==^lTKOuzd@1aznobOQA%`(-KnS?qKnQyGflXO7>{OhU zD!uXPuyotCNk5RWA|~K_p%}Q)yO?CrH@p9OV)%5hdh4G~E!_LYPj^f^TFS>Sta;?j z(NBA~-?(Sa4RhvP2@H^-{`=2gytt+Eh05zM&aU`!;mre;1J&)jDlWd>zk6Wl?Ck!^ z^G{aXT;1M2G;rjF1-lo#v+&%xz6FV);=#6CXBji}*4}>x+qU+$ojLZy-qzr^&An}( z&%N8au&L*h!#mGz``7V`HM7I>qOl|8(H~!RM*GEe$*amwgieIGy5TV2ePi;!|rnaOWTfA}cSMMi_cg*|G^@AJljlcC>&+*pAU;E0B!3~NH zEdTv;UEA+yY<#xZ_v+FQxZbVJUj%0vAFMAalb&jO{>n3bsmJEu!Y*to+I#HuADd@a zy!g({B~?9N_Vh7TKHq~|4oCidUC)g>i}u|xx4#4>*xI9|51;HlfBQe~xw7$@X)k{I z8{gjdjt;l~GIGanBlB-RxZvRa%}pEe$!FEMKWp06Up;h9Y3i;$+y48>>YK{SuRd^k z`L;8-@2MwBnpS*Q(R54dqmNUI&R(isbZB_p<4>g4?kS#mNc-%w^?w)+o!fDF)#g>n R%kyXKuPFNJcNcQo=6|{bEo%S( diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/3aa2fe75-8006-42c8-97d9-5e4835aa023e-m0.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/3aa2fe75-8006-42c8-97d9-5e4835aa023e-m0.avro deleted file mode 100644 index 091dea3c22ecda2c1fd843d44adc425d51d4d29e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6126 zcmb_g2~ZSg7UobOiZHO4LlJ-4V>J#i2lr}%N(>^GXbOYQ4m~{$Gt$#NboU_W&aNoB zX4h=wSP_G;rR#|oE5>-CMqIVT6^#n6R060aRgkD?VnV{Ye|IxIGoSi7+f}!dOI#r0a1)%OH_US0Q>W5-FD=Bx>N^ zz$k+eXZ5U}BGvE{sg0$c%z*#Xc$P^|4A)=)br}fkz4jU?3vU&r~u&BXkMauTkE5Hg9?Qs%fovY@{GhnR4 zdz%lYP!Xari=zfRxF9?Ft+8At+U8i#n@&(DN2ynwRc_^0?!b+#bXqxZBP)%val&e* zrA!(E?TJ9!io9;B^U~bJA3qeT^tB zXfc2>;#jdGlq*A-Xbk5~;dw3ZLkY`duske{l1w^98=#a0w=9=ZkGMs3nN^6rYv|8O z=F1tPY(O?nGf@9d3Li9^rUEfai(?*IJe@#U+ykRX7N+Z_MOpN9{&V3l)iW(!20$uc z6bizrKTq>YGDc z%?0R{titMWpe5)jVlPK>rA=;p6*vqj4C05(gK>et55N^N5^%Mb0*c1CUjWhx0@|x? zJQ_xn)%6YpscJ2^MJQsdI0{3#0R%oso)1>&Ssj?S2=FY!?Rmlb7n14|!G;;!*y9_U z7-2}K5f#%X)(|QnN)ZBK6*1sdEJ9Tv6lo5l6QXv~|D0%Hj zAu%z+q=l`BpGN>CwIBgau*a60ROj#jR0;*!y7CMAIyacrZ zr_~yWJKR_ylC}*=5i*?6DndD)hKWH2-BT5z{X_fGsiO!RK5Tic0FRYDh$bVvf4&OD zVI52guA3@9kzidkK zyHLhlom{-7Y+GrGUq#`P!Z$0*DpCr! zl)CyP9*C(6xHLcD_O-a_fe9Z6B}!wjUJ43GSeF@fYp>^2`Zo&-m95G}gsSNeta-v* z_O$!P^X5s<#zaLmL^XuVuTKlO)wHj;9Ip{GzQDBpS=|R z!68t!=Wy`R{GhY)X|kIk`SV?F9j;cS`o(YQDeFv5d*d z4fXxwrE|yDshS-R)sM?AICydVk_&#Sob!mhCFt1AgS(2P3e~K{Ee{f&C>=u9)}fAG z1X$x7ZOn7?lbmx!MnvUHHb@-3G+>TXbWEPxTM~a)Bq<(iPHWaQqs^EkP11Gr(z5$s zjh-zBn;Q>PZy)j5>9$-8&@jYjQO^-QI6+Z=9>~ZEt$dbH35xW8Eh2)mtvq zgGrC$C+&JspYo$KJ!;R&Oz)~l?Dn{+8%+6I>1+aEW#} zpSSkj<&VZJyZaw@gI{WPyq|8mga4j~eTcK(%}{?X^UEY(*T*Fd2WG~$mHX!>&+(c0 z`GPn@O2ZfzRq5Pa*g>N0+`_MJG)$2iPE0m!>yRB?6NX-FQOnksyI{L-oLqC>f2G8y zV9gsr--5Z_N7rg_&v(wa<~@4!eAT&}=Q%I#-rdn17CNWRr-bd-BNhjnNO=8lL%Jm%S&(x0gn@o_MD)SlQL6 z%Zf{US`hd4)g?%2Yf5F^(#j`bWrpJ2GdY-JW%pGN_1BRrpOBKw&;8wl^B;TWbr5H~ zk-Pp?Ki)qr>7G(QFJ}4Z&Lf_?{`HeC@V2D&n|r6azIzh?jVvFyPH>5OQzCIhHaLx% z44g;SM)~fEa(?k|ui0Lcw@=$1)7703^S_%l|5#0qAjj(5(0r`rMS0Y;(`!!r7AYaF zd5(IWiA4;ll@p&QxGr40KayCxs`}GUuUu_={8QnV`i~BqKJzasYT3DYPN!@2*xecC z`pvP4>mR*Kb)Ray8Tj4q&^yOxbab>F8M}u%PB!0mDj?0rm;LF^hpR`F{eADvz4x}2 zdRH|mN-qDmHaWq+Vxq@`)@M-0Imgz;nZdIO4c6*XC}IsdLKG%qSXz&>B7adPNvKtfC@if|M5;t#5`U2v)1WtS zN~0rKk|imv0)FCeVM%JU;Qus{t0!p!8f9W?hA{f`9;AW?hQOE}8p{M@4aVZxJqjEs z*k;o32v)Xgs;iWP1WO%3lT;558!T9|q=sNvOrtX+W%=k0V1)?wgcf3LP;lkxHrAmX z&3jTPxnRr^n8pe&*d1NgSRxi|3(V)uR8tsgsW+TeV(u+N_l?Z$G`IUk<~GVB)Mhgk zrB|wnE;2N?5QgJDuroJMdl_>GizB`f&2)bePt1UJ>X9mB4=G6paJ+=i5jE3|-a3rt zxfsCc2>c6MNLPl^(>TGI!tKWlv?kGSEU*<&$h)69fGWr7x3oWK&vMHA9)^?}7nZ#_C zCerHFu8YMw@$VS#%x0$4xP%vHk4(zRO zgt!6)=#Z?`?6A8fXfI+1M-rJuZd?@*h7=C*6Gg+gfbS203K<4a?fn5oNEmlI>qWm1q3QWAj~8N9E$~L%q`u1g4Z-PdSqf2EfGr*LZ+fkH^~P;y#rXi zPEGjpNMb;VF@y?0l-X^&5w-7Y@@=w>ouG>t(Sh04Sd8Ys?wDqw{yEcB0?5<)l= z&n%lj5@MBk9;ViFq&EdMo!`nP)R>M+ucop{98=HW7Cs=-U^BijQ5961R|so()l@z~ zE0hp-xUoVcEd@y$)R)jogHfM`2tj(RsnX!Cp?&Stk%sggwj5S~!^#>&y$;?#-vnZx z4kjs5Q`sL-Q|bRv)7OG1>q9x^uzDScaHuLY1dE|XM`)7vJA%w&7Zj$uh|e`0HtCy2 z<0LIk8+6DW8UikIz>awX%!?7`U1pSeF9%MCAnRz ztBS`)!Ch5H6AZeL!!gak5b$;bL!i44+>}X%ONC#lOdB5!mu{{$rUx2KOJ>6Fg%r>Y zZ4eo4wj1_Nx|DG+?9nfc>)fkzn10_n;L7qnFaLXf?AeyHX~&xy8d^*I0%W6qn5c^oo|r*uhl8nM%MWUjjjiEG4<8H68SjU*zw^bclk)dR%mGHv;))UP(JOZ<4lP{qcSFUnUaQa3pMSiB=?*p%C&*7#%{%i4PB;Qr_L+QJsy zo9pFS^1|!>pNflX7HC%{%txjn}rHPK;RO?0O2`pd{khU7HO{KZuB^uyx)a)hu077I#zT&NLVF_lz8J zHT`kP>II*V@RN-G(B`XC*G|R_8N4GpA=10_PU&iqx3hC@$+6JpW*?VWo6F8`C-!sQ z?KxD{{~Pkyufy8L%#Vn-ok>l7D>-XkN<;tLMG?8tMMbeiRjYHZvd&775CCF2UVWelLoni>{@n?L+HD*DKkDAz$5hZmQ{92uSz zmEXG7`MWW%u0MWpWZ~Igo*F%`8GdWKbMa?t(7R3!n>Fe~0pVL7%pHHds50NU^YZQR zkP7j+tYGV?ttb7$ z-5aNl*J?b<N%4|@-$=at zukVtd$E@Yr5-ZIQn{JrR57`@d~yduK##!zLSd8{3t|4%Ie} zF~!c+3!Ik^7#uMm@4@0qANK^u1fSv0UmP`(z+?89;xgB(r+i+OPV=pZ${k5ud9twf zM(vt_#2E_X@2bMFlS_Z{NV^-p^q&{%V~Gje7bp!?QZ};nu*W=8Ez6 z7Ddi~H*|hs>xg|d4~Okb-tc?$rH_w~bIZP7ka6!xq(g1d@68u{Z3fD7lKk!`ou677 tnz8;=|IJJ40%s*1PhI12Ye({jYyZAO>YiNnR{e>}%KFx&7dy7X{{Wc^CUgJ* diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-3805096703135495509-0-3aa2fe75-8006-42c8-97d9-5e4835aa023e.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-3805096703135495509-0-3aa2fe75-8006-42c8-97d9-5e4835aa023e.avro deleted file mode 100644 index 627db9cfed8e131be4823db53cf8cab2aa7bfaba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1851 zcmb7EZ%7ki9M(l>Y9%CUrLwLv6&IUxI+Y?QEx{7X1X*&vyL-1?-Q7Fy-nquy7b_I9 zAc&wyB8#YqiqQH}AO=xkAS9w@(1%1Z0u%bshyJ{`?dIv}w2$}R?|Giz@A>_ycb-m6 z%aldnl|2ZXI5yvEH`~k=W#wk8#cC_9u#t~lULZjcn>1m`qL1hEWZ3G1A_GmLPjEuX zV99YINdVZ?1|^w8qM;-Mv`J_$gILIHVxfx%7-oi_WX1yp@Hi_-;eZ!Xl}1W}43b!? z*hslJ$g?tea|9_72;sj15IGl=G3^D|6AK~08Fwm zo;+S_^mEu_tT|x}>q9D3Y+OczhoXBK6^llNM?_;0nwW$+MTeHXkfBLtYAI@#B^o>@ z;>B|t8=kj&DJ>^tW( znn6B&`Q}m$UgsG+%gLBDn~ukz^+3nKLLOpLLOcSk1v-L=0}}GDP=H0-bbSkA6~tkP zZ=qU290IL}`V?YwD=!@%vu5@jl(O+8Vqsx{B!Pf}As!0KeTXijojh?E3d#mAM8{um zW%>ap%47_~Dv=wYGFE7Yz=;qKfv8`e2%P*O!Ql%MMO&17qQ@at1*Ot7lb_@;0UwV^ zx{CUurRI?zO0<)Vrf!`rnxmuP4PYo!tFZdK-W6FjM`jJIijaCDC7an!g+i-npMe2d zfEcg<1D#bGqyJMpcAu3^qCyExn+qQHBy@iszE*j-uVrq)@W;@6u_|ToKt^@vOv&V+ z{owu^V;h`1CJHAmxaWJ%)#eX%-?ELnlQJ7SI(qt3)3aLo(yxBH8T^v-=+NCU(0#PD z=tIixX}KZWbjh`EvZW}sVW^<1k)qFAvXggS-<}?rYE5do((9r|x4kwMoO<8?2F*F1 zrEP2t~1OER&XBl$%(hTb5c>oSC1;WF(SU zR+O)ooS%}K%$JgymXlbLDh3uRPR>ZpOQ&3BwKRwfp(zi28LNlCdi)n z;)2xVc%aK*UV*vN2uXitUJ0u9K2A)5W=&K(|7p z5SIyNcuh!5Nl8tKhb0qiel{jTe_CcvYH@sWekrE+4NQ>qV}x01MP_jcFtX$EnSdMy z7$&5o=A@PYy@1aE01^U4`QY(`_+& z@-aY4d(ldHr8zl36^IO*SX7i)3DN@$!(32t0nLmSK%vZ%)Lc*wMyST0y+NrzF*C0i zm<++TK*|7^iIzx-ASpjTCp9q-GxI>S17(1U!6gV(w;{+q(82-k3niE_u|P>+!IW5< zQv!4@$aa)Wos(alS`?oIOqv+k*$}DFK{B_pB(+$HfT^Vg1vpJbDrAT-6=V^th%5pY z;!3r(1nkaEt&Go2ElEsCEJ?%&Dx`oW(Qr+3;NYdbd!GK4|I;qNK7YlI#_lPX){2yR-L~Rnx*~CH^OUd+ z&Am%{XQf58?Jmqd-k+lO*B~?1DDP>x&4jaM%)wIbnO*<(U5W7j7W7B^PyQvfv`^)R zJB6$6xINmH+8(iOZO3yil~Y9l1<|V<3@)W`Z(2P0w3NfjlA4c~_Djmhm2$HkczOG$ zOhclB-RG%KO@m@@sjTj~ZWg9y>`?D^?10PCmMtDLJ3AtDo*daaXM)AV$E=ey6SYju z;(jw6eRT5b;kOw-3a{ykFz&lIf0^|4(~~R9-1L^N47hP5RpUc2SOt8Y)1d?4*2^AtT`0qpd$z5oCK diff --git a/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-6133585602095476169-0-b2eff7a4-107d-40c3-846c-6d69ba0f72ff.avro b/examples/idsse_warehouse/tracking/tracking_data/metadata/snap-6133585602095476169-0-b2eff7a4-107d-40c3-846c-6d69ba0f72ff.avro deleted file mode 100644 index 936c6d39630c99ba194a41e187406ea2acb0dc6e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1936 zcmb7ETSyd97&cuYOX|gxGHM&TNh&kD&N^F5U{sJrkyenGrn56=b#gB2%o%N~eaMO+ zGopenJ{0(%z1Z!cpc`E*>}Cco_F(Blgi$05Qgorttj;z%y4B;H`M>Y`|L?zS^6#3G zo~g+os8uT{Pv94OD7TwtXbvmLhw^wA27wA?lxGcdGbBxVJ%yy3a?=G~8h^~w zV66^i4(7?aYMC4lnkpsERqob1W~b*hrjDLmw6@Gu|(1T$k#8dnV^(4MuI zK%pQcmOBX*a*E0mxlSS=K#|w*Tlro>22$*=1Y{uqHN*x1s*VTY{Bamfk{D(3Xh;?0 zDkFqfID>eD!U2S{ac(VH=d2e{wR3H$Go}xTaISL#4H3#!6R1!Y6;Z(&W6;<$Bp5oZ z7K9v&Gc!vuvvgVTf{ez(IYma;^YI|6FcHXX06WqE8<@6$?HNk~&ULhflQDP_G60^3 zJZp~0L^Zqr0Ur=VsIi=)+sh{lEO?t|aJQf#+-x=xgVh5Y0}n-ra0!VBtQOb^MjTL; z`bi3)tWCGKAYMTNhQt<{6(k_AdT37}KDXNV@gXgA*d05Vbz1dNm(sN<{wxM zA@fA4G|3R*@L#lRV2G6<0z5##W|hV0{}hkkXJeCyaMH^x-;Ji;ot@W~wSRhYq4kF2 zh@+$Hc5zBkMrwa;-{YZKa^UK(nkA0u)V3paPy1f42-N;Mv%RHjAmwY);DILSShl7k zUoTEgi?la1+}j~*UHeP-x!1o-mDCqA(}f4wm906#iI&LFl%|Vq*{^Q(EZr3Sz;s^j zSRL8g80r~D$6)F4flwfQQB|9;<@}s)vw!S}o+$gVr{wUdY263zOmz<(L<^$HX+@b2 zbEeH&UR;#3hlvd9_s>XdMc>=ptV^d)(#==CoNm=0Zqsopn-1|lmzPW=+Rbobos z=AS3K*Px6;D_5^_^m|dTjU2ecziIU)l4c M;iSQ*XUF^U8 speed=sqrt(2) approx 1.414 - expected = np.array([0.0, np.sqrt(2), np.sqrt(2)]) - actual = container.to_arrow().column("ball/speed").to_numpy() - np.testing.assert_allclose(actual, expected, atol=1e-6) - - def test_add_column_per_player_udf(self, container): - def compute_pos_sum(x, y): - return x.to_numpy() + y.to_numpy() - - container = container.add_column_per_player("pos_sum", compute_pos_sum, on=["x", "y"]) - assert "teams/home/players/player_1/pos_sum" in container.schema().names() - - # home player_1 x: [23.5, 23.6, 23.7], y: [24.2, 24.3, 24.4] - expected = np.array([23.5 + 24.2, 23.6 + 24.3, 23.7 + 24.4]) - actual = container.to_arrow().column("teams/home/players/player_1/pos_sum").to_numpy() - np.testing.assert_allclose(actual, expected) - - def test_add_column_per_player_ops(self, container): - container = container.add_column_per_player( - "speed_rust", tdc.ops.compute_speed, on=["x", "y"] - ) - assert "teams/home/players/player_1/speed_rust" in container.schema().names() - - # home player_1 x: [23.5, 23.6, 23.7], y: [24.2, 24.3, 24.4] - # dx=0.1, dy=0.1 -> speed=sqrt(0.01 + 0.01) = sqrt(0.02) approx 0.1414 - expected = np.array([0.0, np.sqrt(0.02), np.sqrt(0.02)]) - actual = container.to_arrow().column("teams/home/players/player_1/speed_rust").to_numpy() - np.testing.assert_allclose(actual, expected, atol=1e-6) diff --git a/python/tests/test_advanced_features.py b/python/tests/test_advanced_features.py deleted file mode 100644 index 9982dca..0000000 --- a/python/tests/test_advanced_features.py +++ /dev/null @@ -1,102 +0,0 @@ -from datetime import datetime, timedelta, timezone - -import polars as pl -import tracking_data_container as tdc - - -def test_materialize_frame_temporal(container): - """Test materializing frames by datetime and timedelta.""" - base_time = datetime(2023, 10, 1, 12, 0, 0, tzinfo=timezone.utc) - - # 1. By datetime - # Frame 1 timestamp is base_time + 40ms = 1696161600040000 microseconds - dt = base_time + timedelta(milliseconds=40) - frame_dt = container.materialize_frame(dt) - assert frame_dt["frame_id"] == 123457 - - # 2. By timedelta - # 40ms offset should be Frame 1. - td = timedelta(milliseconds=40) - frame_td = container.materialize_frame(td) - assert frame_td["frame_id"] == 123457 - - -def test_melt_pivot_roundtrip(): - """Test that melting and pivoting correctly reconstructs the wide format.""" - data = { - "match/id": ["m1", "m1"], - "timestamp": [1000000, 1040000], - "frame_id": [1, 2], - "period": ["1", "1"], - "ball/x": [10.0, 11.0], - "ball/y": [20.0, 21.0], - "teams/home/players/p1/x": [30.0, 31.0], - } - container = tdc.TrackingDataContainer(data, validate=False) - - # Melt to long format - long_df = container.melt() - assert "player/id" in long_df.columns - assert "entity_key" in long_df.columns - - # Pivot back to wide - pivoted = tdc.TrackingDataContainer.pivot(long_df) - - assert pivoted.num_rows() == 2 - assert "ball/x" in pivoted.schema().names() - assert "teams/home/players/p1/x" in pivoted.schema().names() - - # Check values - df_piv = pivoted.to_polars().sort("frame_id") - assert df_piv["ball/x"].to_list() == [10.0, 11.0] - assert df_piv["teams/home/players/p1/x"].to_list() == [30.0, 31.0] - - -def test_lazy_schema_caching(container): - """Test that lazy schema caching works and propagates.""" - lazy = container.lazy() - - # First call reconstructions schema - lazy.schema() - assert lazy._cached_schema is not None - - # Transformation should propagate/subset cache - selected = lazy.select(["ball/x", "ball/y"]) - assert selected._cached_schema is not None - assert len(selected._cached_schema.names()) == 2 - - # Slice should preserve cache - sliced = selected.slice(0, 1) - assert sliced._cached_schema == selected._cached_schema - - -def test_pivot_global_constants(): - """Test that global constants are correctly handled in pivot.""" - data = { - "match_id": ["constant_match"] * 4, - "competition_id": ["constant_comp"] * 4, - "timestamp": [1000, 1000, 2000, 2000], - "period": ["1", "1", "1", "1"], - "entity_id": ["p1", "p2", "p1", "p2"], - "team_id": ["home", "home", "home", "home"], - "entity_key": ["p1|home", "p2|home", "p1|home", "p2|home"], - "x": [1.0, 2.0, 3.0, 4.0], - "y": [1.0, 2.0, 3.0, 4.0], - } - df = pl.DataFrame(data) - - pivoted = tdc.TrackingDataContainer.pivot(df) - - # Check that constants are back (mapped to internal CDF names by Rust) - res = pivoted.to_polars() - assert "match/id" in res.columns - assert "competition/id" in res.columns - assert res["match/id"][0] == "constant_match" - - assert "competition/id" in res.columns - assert res["competition/id"][0] == "constant_comp" - - # Check pivoted columns - assert "teams/home/players/p1/x" in res.columns - assert "teams/home/players/p2/x" in res.columns - assert res.sort("timestamp")["teams/home/players/p1/x"].to_list() == [1.0, 3.0] diff --git a/python/tests/test_catalog.py b/python/tests/test_catalog.py index 2ba3b6f..ac597c6 100644 --- a/python/tests/test_catalog.py +++ b/python/tests/test_catalog.py @@ -1,93 +1,191 @@ +import polars as pl +import pytest import tracking_data_container as tdc -def test_catalog_lifecycle(tmp_path): - warehouse_path = tmp_path / "warehouse" - catalog = tdc.TrackingDataCatalog(str(warehouse_path)) - catalog.initialize() - - # Create a container with all required fields for Iceberg - # and some constant attributes (jersey_color, player_name) - # Including a NULL value in x to verify it's allowed - data = { - "match/id": ["match_1", "match_1"], - "timestamp": [1000000, 1040000], # microseconds - "frame_id": [100, 101], - "period": ["1", "1"], - "ball/x": [0.0, None], - "ball/y": [0.0, 1.0], - "teams/home/players/p1/x": [10.0, 11.0], - "teams/home/players/p1/y": [20.0, 21.0], - "teams/home/players/p1/jersey_color": ["red", "red"], - "teams/home/players/p1/name": ["Player 1", "Player 1"], - } - container = tdc.TrackingDataContainer(data, validate=False) - - # Save container - normalization should happen here - catalog.save_container(container) - - # Verify normalization: physical table should not have jersey_color - table = catalog._iceberg_catalog.load_table("tracking.tracking_data") - physical_cols = [f.name for f in table.schema().fields] - assert "jersey_color" not in physical_cols - assert "name" not in physical_cols - - # Load container - lazy_loaded = catalog.load_container(match_id="match_1") - assert isinstance(lazy_loaded, tdc.LazyTrackingDataContainer) - - # Schema should include normalized columns - schema_names = lazy_loaded.schema().names() - assert "teams/home/players/p1/jersey_color" in schema_names - assert "teams/home/players/p1/name" in schema_names - - loaded = lazy_loaded.collect() - assert loaded.num_rows() == 2 - assert loaded.is_bound() - - # Check data - df = loaded.to_polars() - assert df["match/id"][0] == "match_1" - # ball should NOT have player/id - assert "ball/player/id" not in df.columns - assert df["ball/x"][0] == 0.0 - assert df["ball/x"][1] is None - - # Check normalized constants - assert df["teams/home/players/p1/jersey_color"][0] == "red" - assert df["teams/home/players/p1/name"][0] == "Player 1" - - -def test_bound_container_save(tmp_path): - warehouse_path = tmp_path / "warehouse" - catalog = tdc.TrackingDataCatalog(str(warehouse_path)) - catalog.initialize() - - # Create and save with required fields - data = { - "match/id": ["match_1"], - "timestamp": [1000000], - "frame_id": [100], - "period": ["1"], - "ball/x": [0.0], - "ball/y": [0.0], - } - container = tdc.TrackingDataContainer(data, validate=False) - catalog.save_container(container) - - # Load (becomes bound) - lazy_bound = catalog.load_container(match_id="match_1") - bound = lazy_bound.collect() - assert bound.is_bound() - - # Modify and save via bound method - # Preserves catalog binding - new_bound = bound.add_column("ball/z", [1.0]) - assert new_bound.is_bound() - - new_bound.save() - - # Load again to verify - final = catalog.load_container(match_id="match_1").collect() - assert "ball/z" in final.to_polars().columns - assert final.to_polars()["ball/z"][0] == 1.0 +class TestIcebergCatalog: + def test_catalog_lifecycle(self, empty_catalog): + # Create a container with all required fields for Iceberg + # and some constant attributes (jersey_color, player_name) + # Including a NULL value in x to verify it's allowed + data = { + "match/id": ["match_1", "match_1"], + "timestamp": [1000000, 1040000], # microseconds + "frame_id": [100, 101], + "period": ["1", "1"], + "ball/x": [0.0, None], + "ball/y": [0.0, 1.0], + "ball/radius": [0.11, 0.11], # Ball constant + "teams/home/players/p1/x": [10.0, 11.0], + "teams/home/players/p1/y": [20.0, 21.0], + "teams/home/players/p1/jersey_color": ["red", "red"], + "teams/home/players/p1/name": ["Player 1", "Player 1"], + } + container = tdc.TrackingDataContainer(data, validate=False) + + # Save container - normalization should happen here + empty_catalog.save_container(container) + + # Verify normalization: physical table should not have jersey_color + table = empty_catalog._iceberg_catalog.load_table("tracking.tracking_data") + physical_cols = [f.name for f in table.schema().fields] + assert "jersey_color" not in physical_cols + assert "name" not in physical_cols + assert "radius" not in physical_cols + + # Load container + lazy_loaded = empty_catalog.load_container(match_id="match_1") + assert isinstance(lazy_loaded, tdc.LazyTrackingDataContainer) + + # Schema should include normalized columns + schema_names = lazy_loaded.schema().names() + assert "teams/home/players/p1/jersey_color" in schema_names + assert "teams/home/players/p1/name" in schema_names + assert "ball/radius" in schema_names + + loaded = lazy_loaded.collect() + assert loaded.num_rows() == 2 + assert loaded.is_bound() + + # Check data + df = loaded.to_polars() + assert df["match/id"][0] == "match_1" + # ball should NOT have player/id + assert "ball/player/id" not in df.columns + assert df["ball/x"][0] == 0.0 + assert df["ball/x"][1] is None + + # Check normalized constants + assert df["teams/home/players/p1/jersey_color"][0] == "red" + assert df["teams/home/players/p1/name"][0] == "Player 1" + assert df["ball/radius"][0] == "0.11" + + def test_bound_container_save(self, iceberg_catalog): + # Load (becomes bound) + lazy_bound = iceberg_catalog.load_container(match_id="match_12345") + bound = lazy_bound.collect() + assert bound.is_bound() + + # Modify and save via bound method + # Preserves catalog binding + new_bound = bound.add_column("ball/z2", [1.0, 1.0, 1.0]) + assert new_bound.is_bound() + + new_bound.save() + + # Load again to verify + final = iceberg_catalog.load_container(match_id="match_12345").collect() + assert "ball/z2" in final.to_polars().columns + assert final.to_polars()["ball/z2"][0] == 1.0 + + def test_iceberg_backend_errors(self, iceberg_catalog): + lazy = iceberg_catalog.load_container(match_id="match_12345") + + # Filter is now supported via deferred evaluation + filtered = lazy.filter(tdc.col("ball/x") > 0) + assert isinstance(filtered, tdc.LazyTrackingDataContainer) + + with pytest.raises(NotImplementedError, match="Iceberg backend does not support"): + lazy.transform_coordinates(tdc.CoordinateSystem(origin="bottom_left")) + + def test_load_non_existent(self, empty_catalog): + # Loading a non-existent match id should return a lazy container that collects to 0 rows + lazy = empty_catalog.load_container(match_id="non_existent") + loaded = lazy.collect() + assert loaded.num_rows() == 0 + + def test_physical_parquet_format(self, iceberg_catalog): + import glob + import os + + # Find the physical parquet files written by Iceberg + warehouse_dir = iceberg_catalog.warehouse_path + parquet_files = glob.glob( + os.path.join(warehouse_dir, "tracking", "tracking_data", "**", "*.parquet"), + recursive=True, + ) + assert len(parquet_files) > 0 + + # Read the parquet files directly with Polars + df = pl.read_parquet(parquet_files[0]) + + # Verify it is in long format with the expected columns + expected_cols = { + "competition_id", + "season_id", + "match_id", + "frame_id", + "period", + "timestamp", + "ball_status", + "entity_type", + "entity_id", + "entity_key", + "x", + "y", + "z", + } + assert set(df.columns) == expected_cols + + +class TestMeltPivot: + def test_melt_pivot_roundtrip(self): + """Test that melting and pivoting correctly reconstructs the wide format.""" + data = { + "match/id": ["m1", "m1"], + "timestamp": [1000000, 1040000], + "frame_id": [1, 2], + "period": ["1", "1"], + "ball/x": [10.0, 11.0], + "ball/y": [20.0, 21.0], + "teams/home/players/p1/x": [30.0, 31.0], + } + container = tdc.TrackingDataContainer(data, validate=False) + + # Melt to long format + long_df = container.melt() + assert "player/id" in long_df.columns + assert "entity_key" in long_df.columns + + # Pivot back to wide + pivoted = tdc.TrackingDataContainer.pivot(long_df) + + assert pivoted.num_rows() == 2 + assert "ball/x" in pivoted.schema().names() + assert "teams/home/players/p1/x" in pivoted.schema().names() + + # Check values + df_piv = pivoted.to_polars().sort("frame_id") + assert df_piv["ball/x"].to_list() == [10.0, 11.0] + assert df_piv["teams/home/players/p1/x"].to_list() == [30.0, 31.0] + + def test_pivot_global_constants(self): + """Test that global constants are correctly handled in pivot.""" + data = { + "match_id": ["constant_match"] * 4, + "competition_id": ["constant_comp"] * 4, + "timestamp": [1000, 1000, 2000, 2000], + "period": ["1", "1", "1", "1"], + "entity_id": ["p1", "p2", "p1", "p2"], + "team_id": ["home", "home", "home", "home"], + "entity_key": ["p1|home", "p2|home", "p1|home", "p2|home"], + "x": [1.0, 2.0, 3.0, 4.0], + "y": [1.0, 2.0, 3.0, 4.0], + } + df = pl.DataFrame(data) + + pivoted = tdc.TrackingDataContainer.pivot(df) + + # Check that constants are back (mapped to internal CDF names by Rust) + res = pivoted.to_polars() + assert "match/id" in res.columns + assert "competition/id" in res.columns + assert res["match/id"][0] == "constant_match" + + assert "competition/id" in res.columns + assert res["competition/id"][0] == "constant_comp" + + # Check pivoted columns + assert "teams/home/players/p1/x" in res.columns + assert "teams/home/players/p2/x" in res.columns + assert res.sort("timestamp")["teams/home/players/p1/x"].to_list() == [1.0, 3.0] diff --git a/python/tests/test_container.py b/python/tests/test_container.py index 0615ab0..7dfc481 100644 --- a/python/tests/test_container.py +++ b/python/tests/test_container.py @@ -1,3 +1,4 @@ +import numpy as np import pytest import tracking_data_container as tdc @@ -6,10 +7,7 @@ class TestBasics: def test_container_init(self, container) -> None: assert isinstance(container, tdc.TrackingDataContainer) assert container.num_rows() == 3 - # In conftest.py: 5 ball cols, 8 player cols, frame_id, timestamp, period, - # match/id, ball_status, teams/home/id, teams/away/id - # Total = 5+8+7 = 20. But let's just check it's >= 18 - assert len(container.schema()) >= 18 + assert len(container.schema()) == 18 def test_data_field_init(self) -> None: field = tdc.DataField("ball/x") @@ -26,7 +24,72 @@ def test_orientation_mode(self, container) -> None: assert container.orientation_mode == tdc.OrientationMode.HomeAway -class TestColumnAddition: +class TestAddColumn: + def test_add_column_array(self, container): + data = [1.0, 2.0, 3.0] + container = container.add_column("ball/new", data) + assert "ball/new" in container.schema().names() + assert container.to_arrow().column("ball/new").to_pylist() == data + + def test_add_column_expr(self, container): + expr = (tdc.col("ball/x") + tdc.col("ball/y")).sqrt() + container = container.add_column("ball/dist", expr) + assert "ball/dist" in container.schema().names() + + # ball/x: [50.0, 51.0, 52.0], ball/y: [12.0, 13.0, 14.0] + expected = np.sqrt(np.array([50.0, 51.0, 52.0]) + np.array([12.0, 13.0, 14.0])) + actual = container.to_arrow().column("ball/dist").to_numpy() + np.testing.assert_allclose(actual, expected) + + def test_add_column_udf(self, container): + def my_udf(x): + return x.to_numpy() + 10.0 + + container = container.add_column("ball/x_plus_10", my_udf, on=["ball/x"]) + assert "ball/x_plus_10" in container.schema().names() + + # ball/x: [50.0, 51.0, 52.0] + expected = np.array([60.0, 61.0, 62.0]) + actual = container.to_arrow().column("ball/x_plus_10").to_numpy() + np.testing.assert_allclose(actual, expected) + + def test_add_column_ops(self, container): + # Test the tdc.ops.compute_speed rust kernel + container = container.add_column( + "ball/speed", tdc.ops.compute_speed, on=["ball/x", "ball/y"] + ) + assert "ball/speed" in container.schema().names() + + # x: [50.0, 51.0, 52.0], y: [12.0, 13.0, 14.0] + # dx=1, dy=1 -> speed=sqrt(2) approx 1.414 + expected = np.array([0.0, np.sqrt(2), np.sqrt(2)]) + actual = container.to_arrow().column("ball/speed").to_numpy() + np.testing.assert_allclose(actual, expected, atol=1e-6) + + def test_add_column_per_player_udf(self, container): + def compute_pos_sum(x, y): + return x.to_numpy() + y.to_numpy() + + container = container.add_column_per_player("pos_sum", compute_pos_sum, on=["x", "y"]) + assert "teams/home/players/player_1/pos_sum" in container.schema().names() + + # home player_1 x: [23.5, 23.6, 23.7], y: [24.2, 24.3, 24.4] + expected = np.array([23.5 + 24.2, 23.6 + 24.3, 23.7 + 24.4]) + actual = container.to_arrow().column("teams/home/players/player_1/pos_sum").to_numpy() + np.testing.assert_allclose(actual, expected) + + def test_add_column_per_player_ops(self, container): + container = container.add_column_per_player( + "speed_rust", tdc.ops.compute_speed, on=["x", "y"] + ) + assert "teams/home/players/player_1/speed_rust" in container.schema().names() + + # home player_1 x: [23.5, 23.6, 23.7], y: [24.2, 24.3, 24.4] + # dx=0.1, dy=0.1 -> speed=sqrt(0.01 + 0.01) = sqrt(0.02) approx 0.1414 + expected = np.array([0.0, np.sqrt(0.02), np.sqrt(0.02)]) + actual = container.to_arrow().column("teams/home/players/player_1/speed_rust").to_numpy() + np.testing.assert_allclose(actual, expected, atol=1e-6) + def test_container_add_column_direct(self, container) -> None: new_values = [1.0, 2.0, 3.0] container = container.add_column("test/new_col", new_values) @@ -61,99 +124,3 @@ def test_container_add_column_replace(self, container) -> None: new_x = [100.0, 200.0, 300.0] container = container.add_column("ball/x", new_x) assert container.to_polars()["ball/x"].to_list() == pytest.approx(new_x) - - -class TestLazy: - def test_container_lazy_collect(self, container) -> None: - lazy_c = container.lazy() - assert isinstance(lazy_c, tdc.LazyTrackingDataContainer) - - # Collect back to eager - eager_c = lazy_c.collect() - assert isinstance(eager_c, tdc.TrackingDataContainer) - assert eager_c.num_rows() == container.num_rows() - - def test_container_lazy_ops(self, container) -> None: - lazy_c = container.lazy() - - # 1. Lazy select - subset_lazy = lazy_c.select("ball/x") - assert isinstance(subset_lazy, tdc.LazyTrackingDataContainer) - assert len(subset_lazy.schema()) == 1 - - # 2. Lazy add column (raw arrays) - new_values = [1.0, 2.0, 3.0] - with pytest.raises(tdc.exceptions.TrackingDataContainerError, match="not yet supported"): - lazy_c.add_column("test/lazy", new_values) - - def test_container_lazy_add_column_callable(self, container) -> None: - lazy_c = container.lazy() - - def compute_sum(x, y): - import polars as pl - - return pl.Series(x.to_numpy() + y.to_numpy()) - - lazy_c = lazy_c.add_column("ball/sum", compute_sum, on=["ball/x", "ball/y"]) - res = lazy_c.collect() - - assert "ball/sum" in res.to_polars().columns - res_list = res.to_polars()["ball/sum"].to_list() - assert res_list[0] == pytest.approx(62.0) - assert res_list[1] == pytest.approx(64.0) - assert res_list[2] == pytest.approx(66.0) - - def test_container_lazy_true_lazy_ops(self, container) -> None: - # Test things that ARE supported lazily (expressions) - lazy_c = container.lazy() - - # Expression-based addition - expr = tdc.col("ball/x").mul(2) - lazy_c = lazy_c.add_column("ball/x2", expr) - - # Filter (use threshold that definitely excludes some rows) - # Sample ball/x are [50.0, 51.0, 52.0] - lazy_c = lazy_c.filter(tdc.col("ball/x").gt(51.5)) - - # Collect - res = lazy_c.collect() - assert "ball/x2" in res.to_polars().columns - assert res.num_rows() == 1 - - def test_container_lazy_transformations(self, container) -> None: - # 1. Lazy transform coordinates - sys = tdc.CoordinateSystem( - x_range=(0, 100), y_range=(0, 50), length_unit="meters", origin="top_left" - ) - container = container.with_coordinate_system(sys) - - target_sys = tdc.CoordinateSystem( - x_range=(-50, 50), y_range=(-25, 25), length_unit="meters", origin="center" - ) - - lazy_c = container.lazy().transform_coordinates(target_sys) - res = lazy_c.collect() - - # Input ball/x was [50.0, 51.0, 52.0] in (0, 100) range - # Output should be in (-50, 50) range -> [0.0, 1.0, 2.0] - assert res.to_polars()["ball/x"].to_list() == pytest.approx([0.0, 1.0, 2.0]) - - # 2. Lazy transform orientation - # Setup container with known state - container = container.with_orientation(tdc.OrientationMode.HomeAway) - # Period values in sample data: ['first_half', 'second_half', 'first_half'] - # HomeAway means flip in second half. - # Flip across center of (0, 100) range: x -> 100 - x - lazy_c = container.lazy().transform_orientation(tdc.OrientationMode.AwayHome) - res = lazy_c.collect() - - # AwayHome means flip in first half. - # We changed from HomeAway (flip 2nd) to AwayHome (flip 1st). - # So frames 0, 1, 2: - # Frame 0: first_half. HomeAway (no flip) -> AwayHome (flip). CHANGE. - # Frame 1: second_half. HomeAway (flip) -> AwayHome (no flip). CHANGE. - # Frame 2: first_half. HomeAway (no flip) -> AwayHome (flip). CHANGE. - # All frames should be flipped. - assert res.to_polars()["ball/x"][0] == pytest.approx(100.0 - 50.0) - assert res.to_polars()["ball/x"][1] == pytest.approx(100.0 - 51.0) - assert res.to_polars()["ball/x"][2] == pytest.approx(100.0 - 52.0) diff --git a/python/tests/test_lazy.py b/python/tests/test_lazy.py new file mode 100644 index 0000000..d6198ed --- /dev/null +++ b/python/tests/test_lazy.py @@ -0,0 +1,160 @@ +from datetime import datetime, timedelta, timezone + +import pytest +import tracking_data_container as tdc + + +class TestDataFusionBackend: + def test_container_lazy_collect(self, parquet_file) -> None: + lazy_c = tdc.scan_parquet(parquet_file) + assert isinstance(lazy_c, tdc.LazyTrackingDataContainer) + + # Collect back to eager + eager_c = lazy_c.collect() + assert isinstance(eager_c, tdc.TrackingDataContainer) + assert eager_c.num_rows() == 3 + + def test_container_lazy_ops(self, parquet_file) -> None: + lazy_c = tdc.scan_parquet(parquet_file) + + # 1. Lazy select + subset_lazy = lazy_c.select("ball/x") + assert isinstance(subset_lazy, tdc.LazyTrackingDataContainer) + assert len(subset_lazy.schema()) == 1 + + # 2. Lazy selector attribute + sel = lazy_c.selector.players("home") + assert isinstance(sel, tdc.selector.Selection) + + # 3. Lazy add column (raw arrays) + new_values = [1.0, 2.0, 3.0] + with pytest.raises(tdc.exceptions.TrackingDataContainerError, match="not yet supported"): + lazy_c.add_column("test/lazy", new_values) + + def test_container_lazy_add_column_callable(self, parquet_file) -> None: + lazy_c = tdc.scan_parquet(parquet_file) + + def compute_sum(x, y): + import polars as pl + + return pl.Series(x.to_numpy() + y.to_numpy()) + + lazy_c = lazy_c.add_column("ball/sum", compute_sum, on=["ball/x", "ball/y"]) + res = lazy_c.collect() + + assert "ball/sum" in res.to_polars().columns + res_list = res.to_polars()["ball/sum"].to_list() + assert res_list[0] == pytest.approx(62.0) + assert res_list[1] == pytest.approx(64.0) + assert res_list[2] == pytest.approx(66.0) + + def test_container_lazy_true_lazy_ops(self, parquet_file) -> None: + # Test things that ARE supported lazily (expressions) + lazy_c = tdc.scan_parquet(parquet_file) + + # Expression-based addition + expr = tdc.col("ball/x").mul(2) + lazy_c = lazy_c.add_column("ball/x2", expr) + + # Filter (use threshold that definitely excludes some rows) + # Sample ball/x are [50.0, 51.0, 52.0] + lazy_c = lazy_c.filter(tdc.col("ball/x").gt(51.5)) + + # Collect + res = lazy_c.collect() + assert "ball/x2" in res.to_polars().columns + assert res.num_rows() == 1 + + def test_container_lazy_transformations(self, parquet_file) -> None: + # 1. Lazy transform coordinates + sys = tdc.CoordinateSystem( + x_range=(0, 100), y_range=(0, 50), length_unit="meters", origin="top_left" + ) + container = tdc.TrackingDataContainer.from_parquet(parquet_file).with_coordinate_system(sys) + + target_sys = tdc.CoordinateSystem( + x_range=(-50, 50), y_range=(-25, 25), length_unit="meters", origin="center" + ) + + lazy_c = container.lazy().transform_coordinates(target_sys) + res = lazy_c.collect() + + # Input ball/x was [50.0, 51.0, 52.0] in (0, 100) range + # Output should be in (-50, 50) range -> [0.0, 1.0, 2.0] + assert res.to_polars()["ball/x"].to_list() == pytest.approx([0.0, 1.0, 2.0]) + + # 2. Lazy transform orientation + # Setup container with known state + container = container.with_orientation(tdc.OrientationMode.HomeAway) + # Period values in sample data: ['first_half', 'second_half', 'first_half'] + # HomeAway means flip in second half. + # Flip across center of (0, 100) range: x -> 100 - x + lazy_c = container.lazy().transform_orientation(tdc.OrientationMode.AwayHome) + res = lazy_c.collect() + + # AwayHome means flip in first half. + # We changed from HomeAway (flip 2nd) to AwayHome (flip 1st). + # So frames 0, 1, 2: + # Frame 0: first_half. HomeAway (no flip) -> AwayHome (flip). CHANGE. + # Frame 1: second_half. HomeAway (flip) -> AwayHome (no flip). CHANGE. + # Frame 2: first_half. HomeAway (no flip) -> AwayHome (flip). CHANGE. + # All frames should be flipped. + assert res.to_polars()["ball/x"][0] == pytest.approx(100.0 - 50.0) + assert res.to_polars()["ball/x"][1] == pytest.approx(100.0 - 51.0) + assert res.to_polars()["ball/x"][2] == pytest.approx(100.0 - 52.0) + + def test_lazy_schema_caching(self, parquet_file): + """Test that lazy schema caching works and propagates.""" + lazy = tdc.scan_parquet(parquet_file) + + # First call reconstructions schema + lazy.schema() + assert getattr(lazy._backend, "_cached_schema", None) is not None + + # Transformation should propagate/subset cache + selected = lazy.select(["ball/x", "ball/y"]) + assert getattr(selected._backend, "_cached_schema", None) is not None + assert len(selected._backend._cached_schema.names()) == 2 + + # Slice should preserve cache + sliced = selected.slice(0, 1) + assert sliced._backend._cached_schema == selected._backend._cached_schema + + +class TestIcebergBackend: + def test_iceberg_lazy_loading(self, iceberg_catalog): + # Load lazily from Iceberg + lazy_loaded = iceberg_catalog.load_container(match_id="match_12345") + assert isinstance(lazy_loaded, tdc.LazyTrackingDataContainer) + + # Test filter + filtered_lazy = lazy_loaded.filter(tdc.col("ball/x").gt(50.5)) + + # Test lazy op (select) + subset_lazy = filtered_lazy.select("ball/x") + + # Collect back to eager + eager_c = subset_lazy.collect() + + assert eager_c.num_rows() == 2 + assert "ball/x" in eager_c.to_polars().columns + assert "ball/y" not in eager_c.to_polars().columns + assert eager_c.to_polars()["ball/x"][0] == 51.0 + assert eager_c.to_polars()["ball/x"][1] == 52.0 + + +def test_materialize_frame_temporal(container): + """Test materializing frames by datetime and timedelta.""" + base_time = datetime(2023, 10, 1, 12, 0, 0, tzinfo=timezone.utc) + + # 1. By datetime + # Frame 1 timestamp is base_time + 40ms = 1696161600040000 microseconds + dt = base_time + timedelta(milliseconds=40) + frame_dt = container.materialize_frame(dt) + assert frame_dt["frame_id"] == 123457 + + # 2. By timedelta + # 40ms offset should be Frame 1. + td = timedelta(milliseconds=40) + frame_td = container.materialize_frame(td) + assert frame_td["frame_id"] == 123457 diff --git a/python/tracking_data_container/__init__.py b/python/tracking_data_container/__init__.py index 08e0f81..530fb1b 100644 --- a/python/tracking_data_container/__init__.py +++ b/python/tracking_data_container/__init__.py @@ -18,8 +18,8 @@ ops, ) from .builder import TrackingDataBuilder -from .catalog import TrackingDataCatalog -from .config import IcebergConfig +from .catalogs.base import Catalog +from .catalogs.iceberg import IcebergConfig, IcebergTrackingDataCatalog from .container import LazyTrackingDataContainer, TrackingDataContainer from .coordinate_system import CoordinateSystem from .io import load_hawkeye, load_sportec, load_statsperform, scan_parquet @@ -36,6 +36,8 @@ ) from .selector import ColumnSelector, Selection +TrackingDataCatalog = IcebergTrackingDataCatalog + # Silence noisy PyIceberg warnings and logs warnings.filterwarnings("ignore", category=UserWarning, module="pyiceberg.*") warnings.filterwarnings("ignore", message=".*Delete operation did not match any records.*") @@ -47,6 +49,8 @@ "LazyTrackingDataContainer", "TrackingDataBuilder", "TrackingDataCatalog", + "IcebergTrackingDataCatalog", + "Catalog", "IcebergConfig", "CoordinateSystem", "Origin", diff --git a/python/tracking_data_container/_tracking_data_container.pyi b/python/tracking_data_container/_tracking_data_container.pyi index 369c27f..850672d 100644 --- a/python/tracking_data_container/_tracking_data_container.pyi +++ b/python/tracking_data_container/_tracking_data_container.pyi @@ -7,6 +7,7 @@ if TYPE_CHECKING: import pyarrow as pa __version__: str +CORE_COLUMNS: list[str] class Attr: """Standardized attribute names for tracking data.""" diff --git a/python/tracking_data_container/backends/__init__.py b/python/tracking_data_container/backends/__init__.py new file mode 100644 index 0000000..57bf3ed --- /dev/null +++ b/python/tracking_data_container/backends/__init__.py @@ -0,0 +1,5 @@ +from .base import LazyBackend +from .datafusion import DataFusionBackend +from .iceberg import IcebergBackend + +__all__ = ["LazyBackend", "DataFusionBackend", "IcebergBackend"] diff --git a/python/tracking_data_container/backends/base.py b/python/tracking_data_container/backends/base.py new file mode 100644 index 0000000..c393bb0 --- /dev/null +++ b/python/tracking_data_container/backends/base.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any + +from .._tracking_data_container import Expr as _Expr +from .._tracking_data_container import OrientationMode +from ..coordinate_system import CoordinateSystem +from ..schema import Fields + +if TYPE_CHECKING: + from ..container import TrackingDataContainer + + +class LazyBackend: + """Base interface for lazy loading backends.""" + + def schema(self) -> Fields: + raise NotImplementedError + + def select(self, items: str | Sequence[str]) -> LazyBackend: + raise NotImplementedError + + def add_column( + self, name: str, value: Any, on: str | Sequence[str] | None = None + ) -> LazyBackend: + raise NotImplementedError + + def collect(self) -> TrackingDataContainer: + raise NotImplementedError + + def slice(self, offset: int, length: int) -> LazyBackend: + raise NotImplementedError + + def filter(self, predicate: _Expr) -> LazyBackend: + raise NotImplementedError + + def transform_coordinates(self, sys: CoordinateSystem) -> LazyBackend: + raise NotImplementedError + + def transform_orientation(self, mode: str | OrientationMode) -> LazyBackend: + raise NotImplementedError + + def __arrow_c_stream__(self, rs: Any = None) -> Any: + raise NotImplementedError diff --git a/python/tracking_data_container/backends/datafusion.py b/python/tracking_data_container/backends/datafusion.py new file mode 100644 index 0000000..3967934 --- /dev/null +++ b/python/tracking_data_container/backends/datafusion.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +import logging +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any + +from .._tracking_data_container import ( + CORE_COLUMNS as _CORE_COLUMNS, +) +from .._tracking_data_container import ( + Expr as _Expr, +) +from .._tracking_data_container import ( + OrientationMode, + _DataField, + _LazyTrackingDataContainer, +) +from .._tracking_data_container import ( + Selection as _Selection, +) +from ..coordinate_system import CoordinateSystem +from ..exceptions import TrackingDataContainerError +from ..schema import DataField, Fields +from ..selector import Selection +from .base import LazyBackend + +if TYPE_CHECKING: + from ..container import TrackingDataContainer + +logger = logging.getLogger(__name__) + +# Constants for performance (shared with container.py) +CORE_COLUMNS = set(_CORE_COLUMNS) + + +class DataFusionBackend(LazyBackend): + """DataFusion backend for lazy evaluation (e.g. Parquet scanning).""" + + def __init__( + self, + container: _LazyTrackingDataContainer, + cached_schema: Fields | None = None, + deferred_add_columns: list[tuple] | None = None, + ): + self._container = container + self._cached_schema = cached_schema + self._deferred_add_columns = deferred_add_columns or [] + + def schema(self) -> Fields: + if self._cached_schema: + return self._cached_schema + self._cached_schema = Fields(self._container.schema) + return self._cached_schema + + def select(self, items: str | Sequence[str]) -> LazyBackend: + inputs = items if isinstance(items, (list, tuple)) else [items] + processed: list[str | _DataField | _Selection] = [] + for i in inputs: + if isinstance(i, str): + if "*" in i: + for m in self.schema().match(i): + processed.append(_DataField.from_name(m)) + else: + processed.append(_DataField.from_name(i)) + elif isinstance(i, DataField): + processed.append(i._field) + elif isinstance(i, Selection): + processed.append(i._inner) + elif isinstance(i, (_DataField, _Selection)): + processed.append(i) + + new_cache = None + if self._cached_schema: + names = [] + for itm in inputs: + if isinstance(itm, str): + names.extend(self._cached_schema.match(itm)) + else: + names.append(str(itm)) + new_cache = Fields(list(dict.fromkeys(names))) + + return DataFusionBackend( + self._container.select(processed), + cached_schema=new_cache, + deferred_add_columns=self._deferred_add_columns, + ) + + def add_column( + self, name: str, value: Any, on: str | Sequence[str] | None = None + ) -> LazyBackend: + if isinstance(value, _Expr): + return DataFusionBackend( + self._container.add_column(name, value), + cached_schema=self._cached_schema, + deferred_add_columns=self._deferred_add_columns, + ) + + if callable(value): + new_deferred = list(self._deferred_add_columns) + new_deferred.append((name, value, on)) + return DataFusionBackend( + self._container, + cached_schema=self._cached_schema, + deferred_add_columns=new_deferred, + ) + + raise TrackingDataContainerError( + "Adding non-expression columns to lazy containers is not yet supported." + ) + + def collect(self) -> TrackingDataContainer: + from ..container import TrackingDataContainer + + try: + inner = self._container.collect() + res = TrackingDataContainer(inner, validate=False) + + for name, value, on in self._deferred_add_columns: + res = res.add_column(name, value, on=on) + + return res + except Exception as e: + raise TrackingDataContainerError(f"Collection failed: {e}") + + def slice(self, offset: int, length: int) -> LazyBackend: + return DataFusionBackend( + self._container.slice(offset, length), + cached_schema=self._cached_schema, + deferred_add_columns=self._deferred_add_columns, + ) + + def filter(self, predicate: _Expr) -> LazyBackend: + return DataFusionBackend( + self._container.filter(predicate), + cached_schema=self._cached_schema, + deferred_add_columns=self._deferred_add_columns, + ) + + def transform_coordinates(self, sys: CoordinateSystem) -> LazyBackend: + return DataFusionBackend( + self._container.transform_coordinates(sys._inner), + cached_schema=self._cached_schema, + deferred_add_columns=self._deferred_add_columns, + ) + + def transform_orientation(self, mode: str | OrientationMode) -> LazyBackend: + return DataFusionBackend( + self._container.transform_orientation(str(mode)), + cached_schema=self._cached_schema, + deferred_add_columns=self._deferred_add_columns, + ) + + def __arrow_c_stream__(self, rs: Any = None) -> Any: + return self._container.__arrow_c_stream__(rs) diff --git a/python/tracking_data_container/backends/iceberg.py b/python/tracking_data_container/backends/iceberg.py new file mode 100644 index 0000000..d9522e5 --- /dev/null +++ b/python/tracking_data_container/backends/iceberg.py @@ -0,0 +1,305 @@ +from __future__ import annotations + +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any, cast + +from pyiceberg.expressions import ( + And, + BooleanExpression, + EqualTo, + Reference, +) + +from .._tracking_data_container import Expr as _Expr +from .._tracking_data_container import OrientationMode +from ..container import CORE_COLUMNS, TrackingDataContainer +from ..coordinate_system import CoordinateSystem +from ..schema import DataField, Fields, TrackedEntity +from .base import LazyBackend + +if TYPE_CHECKING: + from ..catalogs.iceberg import IcebergTrackingDataCatalog + + +class IcebergBackend(LazyBackend): + """A lazy container backend that defers loading from an Iceberg catalog.""" + + def __init__( + self, + catalog: IcebergTrackingDataCatalog, + match_id: str, + player_id: str | None = None, + selection: str | Sequence[str] | None = None, + limit: int | None = None, + **filters: Any, + ): + self._catalog: IcebergTrackingDataCatalog = catalog + self._match_id = match_id + self._player_id = player_id + self._selection = selection + self._limit = limit + self._filters = filters + self._cached_schema: Fields | None = None + self._predicates: list[_Expr] = [] + + def _get_entities_from_metadata( + self, hierarchical_entities: dict[str, Any] + ) -> list[tuple[TrackedEntity, dict[str, Any]]]: + """Flatten hierarchical metadata into a list of (TrackedEntity, attributes) tuples.""" + entities: list[tuple[TrackedEntity, dict[str, Any]]] = [] + + if ball_attrs := hierarchical_entities.get("ball"): + entities.append((TrackedEntity.from_entity_key("ball|none"), ball_attrs)) + + for tid, team_data in hierarchical_entities.get("teams", {}).items(): + team_attrs = {k: v for k, v in team_data.items() if k != "players"} + entities.append((TrackedEntity.from_entity_key(f"team:{tid}|{tid}"), team_attrs)) + for pid, p_attrs in team_data.get("players", {}).items(): + entities.append((TrackedEntity.from_entity_key(f"{pid}|{tid}"), p_attrs)) + + for rid, r_attrs in hierarchical_entities.get("referees", {}).items(): + entities.append((TrackedEntity.from_entity_key(f"referee:{rid}|none"), r_attrs)) + + if self._player_id: + entities = [ + (e, attrs) + for e, attrs in entities + if e.player_id == self._player_id + or (e.is_referee and e.referee_id == self._player_id) + ] + + return entities + + def _get_wide_names_for_entity( + self, entity: TrackedEntity, attributes: dict[str, Any], physical_attr_cols: list[str] + ) -> list[str]: + """Generate all wide CDF field names for a specific entity.""" + prefix = entity.cdf_prefix() + names = [] + for attr in physical_attr_cols: + names.append(f"{prefix}{attr}") + for attr in attributes: + names.append(f"{prefix}{attr}") + if not entity.is_ball: + names.append(f"{prefix}player/id") + if entity.is_player or entity.is_team: + names.append(f"{prefix}team/id") + return names + + def schema(self) -> Fields: + """Reconstruct the wide schema by inspecting entities in the match metadata.""" + if self._cached_schema: + return self._cached_schema + + if self._catalog._tracking_table is None: + self._catalog._tracking_table = self._catalog._iceberg_catalog.load_table( + "tracking.tracking_data" + ) + + metadata = self._catalog.get_metadata(self._match_id) + entities_hier = metadata.get("_entities") + if entities_hier is None: + raise ValueError(f"Metadata for match {self._match_id} is missing unique entity list.") + + physical_names = {f.name for f in self._catalog._tracking_table.schema().fields} + ignored = { + "match_id", + "competition_id", + "season_id", + "timestamp", + "frame_id", + "period", + "period_id", + "entity_type", + "entity_id", + "entity_key", + } + attr_cols = [c for c in physical_names if c not in ignored and c != "id"] + + wide_names = [ + c + for c in CORE_COLUMNS + if c not in ("player/id", "team/id", "entity_key") + and DataField(c).to_iceberg_name() in physical_names + ] + + for entity, attrs in self._get_entities_from_metadata(entities_hier): + wide_names.extend(self._get_wide_names_for_entity(entity, attrs, attr_cols)) + + self._cached_schema = Fields(list(dict.fromkeys(wide_names))) + return self._cached_schema + + def select(self, items: str | Sequence[str]) -> IcebergBackend: + """Translate wide selection to long selection pushdown.""" + backend = IcebergBackend( + self._catalog, + self._match_id, + self._player_id, + selection=items, + limit=self._limit, + **self._filters, + ) + backend._predicates = list(self._predicates) + return backend + + def slice(self, offset: int, length: int) -> IcebergBackend: + """Apply limit to the lazy container.""" + backend = IcebergBackend( + self._catalog, + self._match_id, + self._player_id, + selection=self._selection, + limit=length, + **self._filters, + ) + backend._predicates = list(self._predicates) + return backend + + def collect(self) -> TrackingDataContainer: + """Finally perform the Iceberg scan and pivot.""" + if self._catalog._tracking_table is None: + self._catalog._tracking_table = self._catalog._iceberg_catalog.load_table( + "tracking.tracking_data" + ) + + filter_expr: BooleanExpression = EqualTo( + term=Reference("match_id"), value=cast(Any, self._match_id) + ) # type: ignore + if self._player_id: + filter_expr = And( + filter_expr, + EqualTo(term=Reference("entity_id"), value=cast(Any, self._player_id)), # type: ignore + ) + for k, v in self._filters.items(): + filter_expr = And( + filter_expr, + EqualTo(term=Reference(k), value=cast(Any, v)), # type: ignore + ) + + # 1. Column Pushdown Calculation + selected_long_fields = None + deferred_select = None + metadata = self._catalog.get_metadata(self._match_id) + entities_hier = metadata.get("_entities", {}) + parsed_entities = self._get_entities_from_metadata(entities_hier) + + if self._selection: + eager_schema = self.schema() + literal_names = ( + eager_schema.match(self._selection) + if isinstance(self._selection, str) and "*" in self._selection + else ( + [self._selection] if isinstance(self._selection, str) else list(self._selection) + ) + ) + deferred_select = list(dict.fromkeys(literal_names)) + + needed_long_internal = { + DataField(name).attribute if name not in CORE_COLUMNS else name + for name in deferred_select + } | set(CORE_COLUMNS) + physical_names = {f.name for f in self._catalog._tracking_table.schema().fields} + selected_long_fields = [ + DataField(f).to_iceberg_name() + for f in needed_long_internal + if DataField(f).to_iceberg_name() in physical_names + ] + + for req in [ + "match_id", + "entity_type", + "entity_id", + "frame_id", + "timestamp", + "period", + "x", + "y", + ]: + if req in physical_names and req not in selected_long_fields: + selected_long_fields.append(req) + + # 2. Perform the scan + scan_kwargs: dict[str, Any] = {"row_filter": filter_expr} + if selected_long_fields: + scan_kwargs["selected_fields"] = tuple(selected_long_fields) + if self._limit: + scan_kwargs["limit"] = self._limit * 50 + + arrow_table = self._catalog._tracking_table.scan(**scan_kwargs).to_arrow() + + # 3. Pivot using Rust core + mapping: dict[str, str] = {} + for entity, _ in parsed_entities: + eid = ( + str(entity.player_id) + if entity.is_player + else ( + f"referee:{entity.referee_id}" + if entity.is_referee + else (f"team:{entity.team_id}" if entity.is_team else "ball") + ) + ) + mapping[eid] = str(entity.team_id) if entity.team_id else "none" + + wide_container = TrackingDataContainer.pivot(arrow_table, entity_to_team=mapping) + + wide_container._catalog = self._catalog + + # 4. Inject constant attributes from metadata + from .._tracking_data_container import lit + + wide_names = wide_container.schema().names() + names_to_add = [] + values_to_add = [] + for entity, attrs in parsed_entities: + prefix = entity.cdf_prefix() + for attr, value in attrs.items(): + wide_name = f"{prefix}{attr}" + if wide_name not in wide_names: + names_to_add.append(wide_name) + values_to_add.append(lit(value)) + + if names_to_add: + wide_container = wide_container.add_columns(names_to_add, values_to_add) + + if hasattr(self, "_predicates") and self._predicates: + lazy_wide = wide_container.lazy() + for p in getattr(self, "_predicates"): + lazy_wide = lazy_wide.filter(p) + wide_container = lazy_wide.collect() + if deferred_select: + wide_container = wide_container.select(deferred_select) + if self._limit: + wide_container = wide_container.slice(0, self._limit) + + return wide_container + + def add_column( + self, name: str, value: Any, on: str | Sequence[str] | None = None + ) -> LazyBackend: + raise NotImplementedError("Iceberg backend does not support adding columns lazily yet.") + + def filter(self, predicate: _Expr) -> LazyBackend: + backend = IcebergBackend( + self._catalog, + self._match_id, + self._player_id, + selection=self._selection, + limit=self._limit, + **self._filters, + ) + backend._predicates = list(self._predicates) + [predicate] + return backend + + def transform_coordinates(self, sys: CoordinateSystem) -> LazyBackend: + raise NotImplementedError( + "Iceberg backend does not support coordinate transformation lazily yet." + ) + + def transform_orientation(self, mode: str | OrientationMode) -> LazyBackend: + raise NotImplementedError( + "Iceberg backend does not support orientation transformation lazily yet." + ) + + def __arrow_c_stream__(self, rs: Any = None) -> Any: + raise NotImplementedError("Iceberg backend does not support __arrow_c_stream__ directly.") diff --git a/python/tracking_data_container/catalog.py b/python/tracking_data_container/catalog.py deleted file mode 100644 index 211784b..0000000 --- a/python/tracking_data_container/catalog.py +++ /dev/null @@ -1,664 +0,0 @@ -"""TrackingDataCatalog - Manages container storage and lifecycle.""" - -from __future__ import annotations - -import json -import logging -from collections.abc import Sequence -from pathlib import Path -from typing import TYPE_CHECKING, Any, cast - -import polars as pl -import pyarrow as pa -from pyiceberg.catalog import load_catalog -from pyiceberg.exceptions import NoSuchTableError -from pyiceberg.expressions import ( - And, - BooleanExpression, - EqualTo, - Reference, -) -from pyiceberg.io.pyarrow import _pyarrow_to_schema_without_ids -from pyiceberg.partitioning import PartitionField, PartitionSpec -from pyiceberg.schema import Schema -from pyiceberg.table import Table -from pyiceberg.transforms import IdentityTransform -from pyiceberg.types import ( - FloatType, - LongType, - NestedField, - StringType, - TimestampType, -) - -from .config import IcebergConfig -from .container import CORE_COLUMNS, LazyTrackingDataContainer, TrackingDataContainer -from .schema import DataField, Fields, TrackedEntity - -if TYPE_CHECKING: - pass - -logger = logging.getLogger(__name__) - -# Mapping from internal CDF-like names to snake_case Iceberg names -# Note: team/id is handled as a constant attribute in metadata -COLUMN_MAPPING = { - "match/id": "match_id", - "competition/id": "competition_id", - "season/id": "season_id", - "player/id": "entity_id", -} - -# Reverse mapping -REVERSE_COLUMN_MAPPING = {v: k for k, v in COLUMN_MAPPING.items()} - - -class TrackingDataCatalog: - """ - Manages storage and lifecycle of TrackingDataContainer instances. - - Backed by Apache Iceberg tables with configurable optimization settings. - Follows Iceberg's Table↔Catalog pattern where containers hold a - reference to their parent catalog. - """ - - def __init__( - self, - warehouse_path: str, - config: IcebergConfig | None = None, - catalog_type: str = "sql", - catalog_properties: dict | None = None, - ): - """ - Initialize catalog. - - Args: - warehouse_path: Path to Iceberg warehouse - config: Iceberg configuration (partitioning, compression, etc.) - catalog_type: Catalog backend (sql, glue, rest, etc.) - catalog_properties: Additional catalog properties - """ - self.warehouse_path = Path(warehouse_path).absolute() - self.config = config or IcebergConfig() - self.catalog_type = catalog_type - - # Create warehouse directory if it doesn't exist - self.warehouse_path.mkdir(parents=True, exist_ok=True) - - # Default properties for SQL catalog (SQLite for POC) - import multiprocessing - - num_workers = multiprocessing.cpu_count() - - default_properties = { - "uri": f"sqlite:///{self.warehouse_path / 'catalog.db'}", - "warehouse": f"file://{self.warehouse_path}", - "py-io-impl": "pyiceberg.io.pyarrow.PyArrowFileIO", - "max-workers": str(num_workers), - } - - self.catalog_properties = catalog_properties or default_properties - - # Load Iceberg catalog - self._iceberg_catalog = load_catalog( - "default", - **{ - "type": self.catalog_type, - **self.catalog_properties, - }, - ) - - self._tracking_table: Table | None = None - self._metadata_table: Table | None = None - - def initialize(self) -> None: - """ - Initialize Iceberg tables (tracking_data + match_metadata). - - Creates tables with configured partitioning and optimization settings. - Must be called once before first use. Idempotent - safe to call multiple times. - """ - try: - self._iceberg_catalog.create_namespace("tracking") - except Exception: - pass # Namespace already exists - - self._create_tracking_table() - self._create_metadata_table() - - def _create_tracking_table(self) -> None: - """Create tracking_data Iceberg table in the requested long format.""" - table_name = "tracking.tracking_data" - - try: - self._tracking_table = self._iceberg_catalog.load_table(table_name) - return - except NoSuchTableError: - pass - - # Define schema. Spatial columns are optional but mandatory during ingestion. - schema = Schema( - NestedField(1, "competition_id", StringType(), required=False), - NestedField(2, "season_id", StringType(), required=False), - NestedField(3, "match_id", StringType(), required=True), - NestedField(4, "frame_id", LongType(), required=True), - NestedField(5, "period", StringType(), required=True), - NestedField(6, "timestamp", TimestampType(), required=True), - NestedField(7, "entity_type", StringType(), required=True), - NestedField(8, "entity_id", StringType(), required=True), - NestedField(9, "x", FloatType(), required=False), - NestedField(10, "y", FloatType(), required=False), - NestedField(11, "z", FloatType(), required=False), - ) - - # Build PartitionSpec from config - partition_fields = [] - for field_name in self.config.partition_by: - field = schema.find_field(field_name) - partition_fields.append( - PartitionField( - source_id=field.field_id, - field_id=1000 + field.field_id, - transform=IdentityTransform(), - name=field_name, - ) - ) - partition_spec = PartitionSpec(*partition_fields) - - # Optimization properties - properties = { - "write.parquet.compression-codec": self.config.compression, - "write.parquet.page-size-bytes": str(self.config.page_size), - "write.parquet.row-group-size": str(self.config.row_group_size), - "write.parquet.dict-enabled": str(self.config.use_dictionary).lower(), - } - - self._tracking_table = self._iceberg_catalog.create_table( - table_name, - schema=schema, - partition_spec=partition_spec, - properties=properties, - ) - - def _create_metadata_table(self) -> None: - """Create match_metadata Iceberg table.""" - table_name = "tracking.match_metadata" - - try: - self._metadata_table = self._iceberg_catalog.load_table(table_name) - return - except NoSuchTableError: - pass - - schema = Schema( - NestedField(1, "match_id", StringType(), required=True), - NestedField(2, "competition_id", StringType(), required=False), - NestedField(3, "season_id", StringType(), required=False), - NestedField(4, "metadata_json", StringType(), required=True), - ) - - partition_spec = PartitionSpec( - PartitionField( - source_id=1, field_id=1001, transform=IdentityTransform(), name="match_id" - ) - ) - - self._metadata_table = self._iceberg_catalog.create_table( - table_name, - schema=schema, - partition_spec=partition_spec, - ) - - def load_container( - self, - match_id: str, - player_id: str | None = None, - **filters: Any, - ) -> CatalogLazyTrackingDataContainer: - """Load container from storage lazily.""" - return CatalogLazyTrackingDataContainer( - self, match_id=match_id, player_id=player_id, **filters - ) - - def save_container( - self, - container: TrackingDataContainer, - match_id: str | None = None, - metadata: dict | None = None, - ) -> None: - """Save container to storage (replaces match partition).""" - if match_id is None: - df = container.to_polars().head(1) - match_id = ( - str(df["match/id"][0]) if "match/id" in df.columns else str(df["match_id"][0]) - ) - - entities = self._save_tracking_data(container, match_id) - - final_metadata = metadata.copy() if metadata else {} - final_metadata["_entities"] = entities - self._save_metadata(final_metadata, match_id) - - def commit_container(self, container: TrackingDataContainer) -> None: - """Commit bound container.""" - df = container.to_polars().head(1) - match_id = df["match/id"][0] if "match/id" in df.columns else df["match_id"][0] - self.save_container(container, match_id=str(match_id)) - - def get_metadata(self, match_id: str) -> dict[Any, Any]: - """Load metadata for a match.""" - if self._metadata_table is None: - self._metadata_table = self._iceberg_catalog.load_table("tracking.match_metadata") - - scan = self._metadata_table.scan( - row_filter=EqualTo(term=Reference("match_id"), value=cast(Any, match_id)) # type: ignore - ) - arrow_table = scan.to_arrow() - - if len(arrow_table) == 0: - return {} - - metadata_json = arrow_table.column("metadata_json")[0].as_py() - return cast(dict[Any, Any], json.loads(metadata_json)) - - def _save_tracking_data( - self, - container: TrackingDataContainer, - match_id: str, - ) -> dict[str, Any]: - """Save tracking data to Iceberg table in entity-long format.""" - if self._tracking_table is None: - self._tracking_table = self._iceberg_catalog.load_table("tracking.tracking_data") - - # 1. Melt and add entity types - long_df = container.melt() - if "entity_key" not in long_df.columns: - raise ValueError("entity_key column is required for saving to catalog") - - long_df = long_df.with_columns( - pl.col("entity_key") - .map_elements(lambda k: TrackedEntity.from_entity_key(k).type, return_dtype=pl.String) - .alias("entity_type") - ) - - # 2. Column Mapping - rename_dict = {k: v for k, v in COLUMN_MAPPING.items() if k in long_df.columns} - long_df = long_df.rename(rename_dict).with_columns(pl.lit(match_id).alias("match_id")) - - # 3. Constant Extraction (Normalization) - # Identity and core temporal fields are strictly preserved in tracking table. - # team/id is now moved to metadata as a constant. - core_cols = { - "match_id", - "frame_id", - "period", - "timestamp", - "entity_type", - "entity_id", - "x", - "y", - "z", - "entity_key", - } - potential_constants = [c for c in long_df.columns if c not in core_cols and c != "team/id"] - - # Group by entity AND its original team attribute to extract constants correctly - # team/id is part of the key to identify constants per-entity-instance - agg_stats = long_df.group_by(["entity_id", "team/id", "entity_type"]).agg( - [pl.col(c).n_unique().alias(f"{c}_count") for c in potential_constants] - + [pl.col(c).first().alias(f"{c}_val") for c in potential_constants] - ) - - constants_to_move = [c for c in potential_constants if agg_stats[f"{c}_count"].max() == 1] - - # Prepare hierarchical metadata - entities_flat = ( - agg_stats.select( - ["entity_id", "team/id", "entity_type"] + [f"{c}_val" for c in constants_to_move] - ) - .rename({f"{c}_val": c for c in constants_to_move}) - .to_dicts() - ) - - hierarchical_meta: dict[str, Any] = {"ball": {}, "teams": {}, "referees": {}} - for ent in entities_flat: - etype, eid, tid = ( - ent.pop("entity_type"), - ent.pop("entity_id"), - ent.pop("team/id", "none"), - ) - attrs = {k: v for k, v in ent.items() if v is not None} - - if etype == "ball": - hierarchical_meta["ball"] = attrs - elif etype == "referee": - hierarchical_meta["referees"][eid] = attrs - elif etype == "player": - team = hierarchical_meta["teams"].setdefault(tid, {"players": {}}) - team["players"][eid] = attrs - elif etype == "team": - target_tid = tid if tid != "none" else eid.replace("team:", "") - team = hierarchical_meta["teams"].setdefault(target_tid, {"players": {}}) - team.update(attrs) - - # Drop constant columns AND the team/id column from physical table - long_df = long_df.drop([c for c in constants_to_move if c in long_df.columns] + ["team/id"]) - - # 4. Final Validation & Save - for field in ["x", "y"]: - if field not in long_df.columns: - raise ValueError(f"Required column {field} is missing") - - must_not_be_null = [ - "match_id", - "frame_id", - "period", - "timestamp", - "entity_type", - "entity_id", - ] - for field in must_not_be_null: - if field not in long_df.columns: - raise ValueError(f"Required field {field} is missing from container") - if long_df[field].null_count() > 0: - raise ValueError(f"Required field {field} contains null values") - - # Convert to Arrow - arrow_table = long_df.to_arrow() - known_types = { - "timestamp": pa.timestamp("us"), - "frame_id": pa.int64(), - "x": pa.float32(), - "y": pa.float32(), - "z": pa.float32(), - } - - new_cols, new_fields = [], [] - for i, field in enumerate(arrow_table.schema): - target = known_types.get(field.name) or ( - field.type.value_type - if isinstance(field.type, pa.DictionaryType) - else (pa.string() if pa.types.is_null(field.type) else None) - ) - new_cols.append(arrow_table.column(i).cast(target) if target else arrow_table.column(i)) - new_fields.append(pa.field(field.name, target, field.nullable) if target else field) - - arrow_table = pa.Table.from_arrays(new_cols, schema=pa.schema(new_fields)) - - with self._tracking_table.transaction() as txn: - txn.update_schema().union_by_name( - _pyarrow_to_schema_without_ids(arrow_table.schema) - ).commit() - txn.delete(EqualTo(term=Reference("match_id"), value=cast(Any, match_id))) # type: ignore - txn.append(arrow_table) - - return hierarchical_meta - - def _save_metadata(self, metadata: dict, match_id: str) -> None: - """Save metadata to Iceberg table.""" - if self._metadata_table is None: - self._metadata_table = self._iceberg_catalog.load_table("tracking.match_metadata") - - metadata_schema = pa.schema( - [ - pa.field("match_id", pa.string(), nullable=False), - pa.field("competition_id", pa.string(), nullable=True), - pa.field("season_id", pa.string(), nullable=True), - pa.field("metadata_json", pa.string(), nullable=False), - ] - ) - - metadata_row = pa.table( - { - "match_id": [match_id], - "competition_id": [metadata.get("competition_id")], - "season_id": [metadata.get("season_id")], - "metadata_json": [json.dumps(metadata)], - }, - schema=metadata_schema, - ) - - with self._metadata_table.transaction() as txn: - txn.delete(EqualTo(term=Reference("match_id"), value=cast(Any, match_id))) # type: ignore - txn.append(metadata_row) - - def __repr__(self) -> str: - return ( - f"TrackingDataCatalog(" - f"warehouse={self.warehouse_path}, " - f"catalog_type={self.catalog_type})" - ) - - -class CatalogLazyTrackingDataContainer(LazyTrackingDataContainer): - """A lazy container that defers loading from an Iceberg catalog.""" - - def __init__( - self, - catalog: TrackingDataCatalog, - match_id: str, - player_id: str | None = None, - selection: str | Sequence[str] | None = None, - limit: int | None = None, - **filters: Any, - ): - super().__init__(None, catalog=catalog, is_long_storage=True) # type: ignore - self._catalog: TrackingDataCatalog = catalog - self._match_id = match_id - self._player_id = player_id - self._selection = selection - self._limit = limit - self._filters = filters - self._cached_schema: Fields | None = None - - def _get_entities_from_metadata( - self, hierarchical_entities: dict[str, Any] - ) -> list[tuple[TrackedEntity, dict[str, Any]]]: - """Flatten hierarchical metadata into a list of (TrackedEntity, attributes) tuples.""" - entities: list[tuple[TrackedEntity, dict[str, Any]]] = [] - - if ball_attrs := hierarchical_entities.get("ball"): - entities.append((TrackedEntity.from_entity_key("ball|none"), ball_attrs)) - - for tid, team_data in hierarchical_entities.get("teams", {}).items(): - team_attrs = {k: v for k, v in team_data.items() if k != "players"} - entities.append((TrackedEntity.from_entity_key(f"team:{tid}|{tid}"), team_attrs)) - for pid, p_attrs in team_data.get("players", {}).items(): - entities.append((TrackedEntity.from_entity_key(f"{pid}|{tid}"), p_attrs)) - - for rid, r_attrs in hierarchical_entities.get("referees", {}).items(): - entities.append((TrackedEntity.from_entity_key(f"referee:{rid}|none"), r_attrs)) - - if self._player_id: - entities = [ - (e, attrs) - for e, attrs in entities - if e.player_id == self._player_id - or (e.is_referee and e.referee_id == self._player_id) - ] - - return entities - - def _get_wide_names_for_entity( - self, entity: TrackedEntity, attributes: dict[str, Any], physical_attr_cols: list[str] - ) -> list[str]: - """Generate all wide CDF field names for a specific entity.""" - prefix = entity.cdf_prefix() - names = [] - for attr in physical_attr_cols: - names.append(f"{prefix}{attr}") - for attr in attributes: - names.append(f"{prefix}{attr}") - if not entity.is_ball: - names.append(f"{prefix}player/id") - if entity.is_player or entity.is_team: - names.append(f"{prefix}team/id") - return names - - def schema(self) -> Fields: - """Reconstruct the wide schema by inspecting entities in the match metadata.""" - if self._cached_schema: - return self._cached_schema - - if self._catalog._tracking_table is None: - self._catalog._tracking_table = self._catalog._iceberg_catalog.load_table( - "tracking.tracking_data" - ) - - metadata = self._catalog.get_metadata(self._match_id) - entities_hier = metadata.get("_entities") - if entities_hier is None: - raise ValueError(f"Metadata for match {self._match_id} is missing unique entity list.") - - physical_names = {f.name for f in self._catalog._tracking_table.schema().fields} - ignored = { - "match_id", - "competition_id", - "season_id", - "timestamp", - "frame_id", - "period", - "period_id", - "entity_type", - "entity_id", - "entity_key", - } - attr_cols = [c for c in physical_names if c not in ignored and c != "id"] - - wide_names = [ - c - for c in CORE_COLUMNS - if c not in ("player/id", "team/id", "entity_key") - and COLUMN_MAPPING.get(c, c) in physical_names - ] - - for entity, attrs in self._get_entities_from_metadata(entities_hier): - wide_names.extend(self._get_wide_names_for_entity(entity, attrs, attr_cols)) - - self._cached_schema = Fields(list(dict.fromkeys(wide_names))) - return self._cached_schema - - def select(self, items: str | Sequence[str]) -> CatalogLazyTrackingDataContainer: - """Translate wide selection to long selection pushdown.""" - return CatalogLazyTrackingDataContainer( - self._catalog, - self._match_id, - self._player_id, - selection=items, - limit=self._limit, - **self._filters, - ) - - def slice(self, offset: int, length: int) -> CatalogLazyTrackingDataContainer: - """Apply limit to the lazy container.""" - return CatalogLazyTrackingDataContainer( - self._catalog, - self._match_id, - self._player_id, - selection=self._selection, - limit=length, - **self._filters, - ) - - def collect(self) -> TrackingDataContainer: - """Finally perform the Iceberg scan and pivot.""" - if self._catalog._tracking_table is None: - self._catalog._tracking_table = self._catalog._iceberg_catalog.load_table( - "tracking.tracking_data" - ) - - filter_expr: BooleanExpression = EqualTo( - term=Reference("match_id"), value=cast(Any, self._match_id) - ) # type: ignore - if self._player_id: - filter_expr = And( - filter_expr, - EqualTo(term=Reference("entity_id"), value=cast(Any, self._player_id)), # type: ignore - ) - - # 1. Column Pushdown Calculation - selected_long_fields = None - deferred_select = None - metadata = self._catalog.get_metadata(self._match_id) - entities_hier = metadata.get("_entities", {}) - parsed_entities = self._get_entities_from_metadata(entities_hier) - - if self._selection: - eager_schema = self.schema() - literal_names = ( - eager_schema.match(self._selection) - if isinstance(self._selection, str) and "*" in self._selection - else ( - [self._selection] if isinstance(self._selection, str) else list(self._selection) - ) - ) - deferred_select = list(dict.fromkeys(literal_names)) - - needed_long_internal = { - DataField(name).attribute if name not in CORE_COLUMNS else name - for name in deferred_select - } | set(CORE_COLUMNS) - physical_names = {f.name for f in self._catalog._tracking_table.schema().fields} - selected_long_fields = [ - COLUMN_MAPPING.get(f, f) - for f in needed_long_internal - if COLUMN_MAPPING.get(f, f) in physical_names - ] - - for req in [ - "match_id", - "entity_type", - "entity_id", - "frame_id", - "timestamp", - "period", - "x", - "y", - ]: - if req in physical_names and req not in selected_long_fields: - selected_long_fields.append(req) - - # 2. Perform the scan - scan_kwargs: dict[str, Any] = {"row_filter": filter_expr} - if selected_long_fields: - scan_kwargs["selected_fields"] = tuple(selected_long_fields) - if self._limit: - scan_kwargs["limit"] = self._limit * 50 - - arrow_table = self._catalog._tracking_table.scan(**scan_kwargs).to_arrow() - - # 3. Pivot using Rust core - mapping: dict[str, str] = {} - for entity, _ in parsed_entities: - eid = ( - str(entity.player_id) - if entity.is_player - else ( - f"referee:{entity.referee_id}" - if entity.is_referee - else (f"team:{entity.team_id}" if entity.is_team else "ball") - ) - ) - mapping[eid] = str(entity.team_id) if entity.team_id else "none" - - wide_container = TrackingDataContainer.pivot(arrow_table, entity_to_team=mapping) - - wide_container._catalog = self._catalog - - # 4. Inject constant attributes from metadata - wide_names = wide_container.schema().names() - for entity, attrs in parsed_entities: - prefix = entity.cdf_prefix() - for attr, value in attrs.items(): - wide_name = f"{prefix}{attr}" - if wide_name not in wide_names: - wide_container = wide_container.add_column( - wide_name, [value] * wide_container.num_rows() - ) - - if deferred_select: - wide_container = wide_container.select(deferred_select) - if self._limit: - wide_container = wide_container.slice(0, self._limit) - - return wide_container diff --git a/python/tracking_data_container/catalogs/__init__.py b/python/tracking_data_container/catalogs/__init__.py new file mode 100644 index 0000000..44f786f --- /dev/null +++ b/python/tracking_data_container/catalogs/__init__.py @@ -0,0 +1,4 @@ +from .base import Catalog +from .iceberg import IcebergTrackingDataCatalog + +__all__ = ["Catalog", "IcebergTrackingDataCatalog"] diff --git a/python/tracking_data_container/catalogs/base.py b/python/tracking_data_container/catalogs/base.py new file mode 100644 index 0000000..1ab1e6b --- /dev/null +++ b/python/tracking_data_container/catalogs/base.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import logging +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from ..container import LazyTrackingDataContainer, TrackingDataContainer + +logger = logging.getLogger(__name__) + + +class Catalog(ABC): + """ + Abstract base class for a TrackingDataCatalog. + + A catalog manages the storage and lifecycle of TrackingDataContainer instances. + It acts as the repository for tracking data, allowing loading, saving, and + querying metadata. + """ + + @abstractmethod + def initialize(self) -> None: + """Initialize the catalog (e.g., create necessary tables/namespaces).""" + pass + + @abstractmethod + def load_container( + self, + match_id: str, + player_id: str | None = None, + **filters: Any, + ) -> LazyTrackingDataContainer: + """ + Load tracking data lazily from the catalog. + + Args: + match_id: The ID of the match to load. + player_id: Optional ID of a specific player to load. + **filters: Additional partition filters (e.g. frame_id ranges). + + Returns: + A lazy container pointing to the catalog data. + """ + pass + + @abstractmethod + def save_container( + self, + container: TrackingDataContainer, + match_id: str | None = None, + ) -> None: + """ + Save a tracking container to the catalog. + + Args: + container: The container to save. + match_id: Override match ID (extracted from container if None). + """ + pass + + @abstractmethod + def commit_container(self, container: TrackingDataContainer) -> None: + """ + Commit in-place modifications to an already-bound container. + + Args: + container: The bound container to commit. + """ + pass + + @abstractmethod + def get_metadata(self, match_id: str) -> dict[Any, Any]: + """ + Retrieve metadata associated with a match in the catalog. + + Args: + match_id: The ID of the match. + + Returns: + A dictionary containing the parsed match metadata. + """ + pass diff --git a/python/tracking_data_container/catalogs/iceberg.py b/python/tracking_data_container/catalogs/iceberg.py new file mode 100644 index 0000000..ef6e8d2 --- /dev/null +++ b/python/tracking_data_container/catalogs/iceberg.py @@ -0,0 +1,439 @@ +"""TrackingDataCatalog - Manages container storage and lifecycle.""" + +from __future__ import annotations + +import json +import logging +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, cast + +import pyarrow as pa +from pyiceberg.catalog import load_catalog +from pyiceberg.exceptions import NoSuchTableError +from pyiceberg.expressions import ( + EqualTo, + Reference, +) +from pyiceberg.io.pyarrow import _pyarrow_to_schema_without_ids +from pyiceberg.partitioning import PartitionField, PartitionSpec +from pyiceberg.schema import Schema +from pyiceberg.table import Table +from pyiceberg.transforms import IdentityTransform +from pyiceberg.types import ( + FloatType, + LongType, + NestedField, + StringType, + TimestampType, +) + +from ..backends.iceberg import IcebergBackend +from ..container import LazyTrackingDataContainer, TrackingDataContainer +from .base import Catalog + + +@dataclass +class IcebergConfig: + """Configuration for Iceberg table optimization.""" + + # Partitioning + partition_by: list[str] = field(default_factory=lambda: ["match_id"]) + + # Parquet optimization + compression: str = "snappy" # or "zstd" + page_size: int = 1024 * 1024 # 1 MB + row_group_size: int = 128 * 1024 * 1024 # 128 MB + + # Storage features + write_page_index: bool = True + write_statistics: bool = True + use_dictionary: bool = True + + # Sorting + sort_by: list[str] = field(default_factory=lambda: ["entity_id", "timestamp"]) + + +logger = logging.getLogger(__name__) + + +class IcebergTrackingDataCatalog(Catalog): + """ + Manages storage and lifecycle of TrackingDataContainer instances. + + Backed by Apache Iceberg tables with configurable optimization settings. + Follows Iceberg's Table↔Catalog pattern where containers hold a + reference to their parent catalog. + """ + + def __init__( + self, + warehouse_path: str, + config: IcebergConfig | None = None, + catalog_type: str = "sql", + catalog_properties: dict | None = None, + ): + """ + Initialize catalog. + + Args: + warehouse_path: Path to Iceberg warehouse + config: Iceberg configuration (partitioning, compression, etc.) + catalog_type: Catalog backend (sql, glue, rest, etc.) + catalog_properties: Additional catalog properties + """ + self.warehouse_path = Path(warehouse_path).absolute() + self.config = config or IcebergConfig() + self.catalog_type = catalog_type + + # Create warehouse directory if it doesn't exist + self.warehouse_path.mkdir(parents=True, exist_ok=True) + + # Default properties for SQL catalog (SQLite for POC) + default_properties = { + "uri": f"sqlite:///{self.warehouse_path / 'catalog.db'}", + "warehouse": f"file://{self.warehouse_path}", + } + + self.catalog_properties = catalog_properties or default_properties + + # Load Iceberg catalog + self._iceberg_catalog = load_catalog( + "default", + **{ + "type": self.catalog_type, + **self.catalog_properties, + }, + ) + + self._tracking_table: Table | None = None + self._metadata_table: Table | None = None + + def initialize(self) -> None: + """ + Initialize Iceberg tables (tracking_data + match_metadata). + + Creates tables with configured partitioning and optimization settings. + Must be called once before first use. Idempotent - safe to call multiple times. + """ + from pyiceberg.exceptions import NamespaceAlreadyExistsError + + try: + self._iceberg_catalog.create_namespace("tracking") + except NamespaceAlreadyExistsError: + pass # Namespace already exists + + self._create_tracking_table() + self._create_metadata_table() + + def _create_tracking_table(self) -> None: + """Create tracking_data Iceberg table in the requested long format.""" + table_name = "tracking.tracking_data" + + # Check if table exists + try: + self._tracking_table = self._iceberg_catalog.load_table(table_name) + return # Table exists + except NoSuchTableError: + pass # Create new table + + # Define schema. + schema = Schema( + NestedField(1, "competition_id", StringType(), required=False), + NestedField(2, "season_id", StringType(), required=False), + NestedField(3, "match_id", StringType(), required=True), + NestedField(4, "frame_id", LongType(), required=True), + NestedField(5, "period", StringType(), required=True), + NestedField(6, "timestamp", TimestampType(), required=True), + NestedField(7, "entity_type", StringType(), required=True), + NestedField(8, "entity_id", StringType(), required=True), + NestedField(9, "x", FloatType(), required=True), + NestedField(10, "y", FloatType(), required=True), + NestedField(11, "z", FloatType(), required=False), + ) + + # Build PartitionSpec from config + partition_fields = [] + for field_name in self.config.partition_by: + field = schema.find_field(field_name) + partition_fields.append( + PartitionField( + source_id=field.field_id, + field_id=1000 + field.field_id, + transform=IdentityTransform(), + name=field_name, + ) + ) + partition_spec = PartitionSpec(*partition_fields) + + # Optimization properties + properties = { + "write.parquet.compression-codec": self.config.compression, + "write.parquet.page-size-bytes": str(self.config.page_size), + "write.parquet.row-group-size": str(self.config.row_group_size), + "write.parquet.dict-enabled": str(self.config.use_dictionary).lower(), + } + + self._tracking_table = self._iceberg_catalog.create_table( + table_name, + schema=schema, + partition_spec=partition_spec, + properties=properties, + ) + + def _create_metadata_table(self) -> None: + """Create match_metadata Iceberg table.""" + table_name = "tracking.match_metadata" + + # Check if table exists + try: + self._metadata_table = self._iceberg_catalog.load_table(table_name) + return + except NoSuchTableError: + pass + + # Define schema (simplified for POC) + schema = Schema( + NestedField(1, "match_id", StringType(), required=True), + NestedField(2, "competition_id", StringType(), required=False), + NestedField(3, "season_id", StringType(), required=False), + NestedField(4, "metadata_json", StringType(), required=True), + ) + + partition_spec = PartitionSpec( + PartitionField( + source_id=1, field_id=1001, transform=IdentityTransform(), name="match_id" + ) + ) + + self._metadata_table = self._iceberg_catalog.create_table( + table_name, + schema=schema, + partition_spec=partition_spec, + ) + + def load_container( + self, + match_id: str, + player_id: str | None = None, + **filters: Any, + ) -> LazyTrackingDataContainer: + """Load container from storage lazily.""" + backend = IcebergBackend(self, match_id=match_id, player_id=player_id, **filters) + return LazyTrackingDataContainer(backend) + + def save_container( + self, + container: TrackingDataContainer, + match_id: str | None = None, + metadata: dict | None = None, + ) -> None: + """Save container to storage (replaces match partition).""" + if match_id is None: + frame = container.materialize_frame(0) + try: + match_id = str(frame["match/id"]) + except KeyError: + match_id = str(frame["match_id"]) + + entities = self._save_tracking_data(container, match_id) + + final_metadata = metadata.copy() if metadata else {} + final_metadata["_entities"] = entities + self._save_metadata(final_metadata, match_id) + + def commit_container(self, container: TrackingDataContainer) -> None: + """Commit bound container.""" + frame = container.materialize_frame(0) + match_id = frame["match/id"] + + self.save_container(container, match_id=str(match_id)) + + def get_metadata(self, match_id: str) -> dict[Any, Any]: + """Load metadata for a match.""" + if self._metadata_table is None: + self._metadata_table = self._iceberg_catalog.load_table("tracking.match_metadata") + + scan = self._metadata_table.scan( + row_filter=EqualTo(term=Reference("match_id"), value=cast(Any, match_id)) # type: ignore + ) + arrow_table = scan.to_arrow() + + if len(arrow_table) == 0: + return {} + + metadata_json = arrow_table.column("metadata_json")[0].as_py() + return cast(dict[Any, Any], json.loads(metadata_json)) + + def _save_tracking_data( + self, + container: TrackingDataContainer, + match_id: str, + ) -> dict[str, Any]: + """Save tracking data to Iceberg table in entity-long format.""" + if self._tracking_table is None: + self._tracking_table = self._iceberg_catalog.load_table("tracking.tracking_data") + + # 1. Melt container natively in DataFusion + lazy_df = getattr(container._container.lazy(), "melt")() + + # 3. Constant Extraction (Normalization) + core_cols = { + "match/id", + "frame_id", + "period", + "timestamp", + "entity_type", + "entity_id", + "x", + "y", + "z", + "entity_key", + "player/id", + "team/id", + } + potential_constants = [] + for sch_field in container.schema(): + if sch_field.entity is not None: + attr = sch_field.attribute + if attr not in ("x", "y", "z") and attr not in core_cols: + potential_constants.append(attr) + potential_constants = list(set(potential_constants)) + + # Extract metadata via DataFusion natively + lazy_df, agg_table = lazy_df.extract_iceberg_metadata(match_id, potential_constants) + + # Build hierarchical meta + agg_dicts = agg_table.to_pylist() + constants_to_move = [] + for c in potential_constants: + # Check if all unique counts are exactly 1 or null (0) + if all(row.get(f"{c}_count") in (1, 0, None) for row in agg_dicts): + constants_to_move.append(c) + + hierarchical_meta: dict[str, Any] = {"ball": {}, "teams": {}, "referees": {}} + for row in agg_dicts: + etype = row["entity_type"] + eid = row["entity_id"] + tid = row.get("team/id") or "none" + + attrs = {} + for c in constants_to_move: + val = row.get(f"{c}_val") + if val is not None: + attrs[c] = val + + if not attrs: + continue + + if etype == "ball": + hierarchical_meta["ball"].update(attrs) + elif etype == "referee": + hierarchical_meta["referees"].setdefault(eid, {}).update(attrs) + elif etype == "player": + team = hierarchical_meta["teams"].setdefault(tid, {"players": {}}) + team["players"].setdefault(eid, {}).update(attrs) + elif etype == "team": + target_tid = tid if tid != "none" else eid.replace("team:", "") + team = hierarchical_meta["teams"].setdefault(target_tid, {"players": {}}) + team.update(attrs) + + # Drop constant columns AND the team/id column from physical table via DataFusion + cols_to_drop = constants_to_move + ["team/id"] + lazy_df = lazy_df.drop_columns(cols_to_drop) + + # Convert to Arrow natively + arrow_table = pa.table(lazy_df) + + # Validation + col_names = arrow_table.column_names + if "entity_key" not in col_names: + raise ValueError("entity_key column is required for saving to catalog") + + # 4. Final Validation & Save + for req_field in ["x", "y"]: + if req_field not in col_names: + raise ValueError(f"Required column {req_field} is missing") + + must_not_be_null = [ + "match_id", + "frame_id", + "period", + "timestamp", + "entity_type", + "entity_id", + ] + for req_field in must_not_be_null: + if req_field not in col_names: + raise ValueError(f"Required field {req_field} is missing from container") + if arrow_table.column(req_field).null_count > 0: + raise ValueError(f"Required field {req_field} contains null values") + + # Convert to Arrow types + known_types = { + "timestamp": pa.timestamp("us"), + "frame_id": pa.int64(), + "x": pa.float32(), + "y": pa.float32(), + "z": pa.float32(), + } + + new_cols, new_fields = [], [] + for i, pa_field in enumerate(arrow_table.schema): + target = known_types.get(pa_field.name) or ( + pa_field.type.value_type + if isinstance(pa_field.type, pa.DictionaryType) + else (pa.string() if pa.types.is_null(pa_field.type) else None) + ) + new_cols.append(arrow_table.column(i).cast(target) if target else arrow_table.column(i)) + new_fields.append( + pa.field(pa_field.name, target, nullable=True) + if target + else pa.field(pa_field.name, pa_field.type, nullable=True) + ) + + arrow_table = pa.Table.from_arrays(new_cols, schema=pa.schema(new_fields)) + + with self._tracking_table.transaction() as txn: + txn.update_schema().union_by_name( + _pyarrow_to_schema_without_ids(arrow_table.schema) + ).commit() + txn.delete(EqualTo(term=Reference("match_id"), value=cast(Any, match_id))) # type: ignore + txn.append(arrow_table) + + return hierarchical_meta + + def _save_metadata(self, metadata: dict, match_id: str) -> None: + """Save metadata to Iceberg table.""" + if self._metadata_table is None: + self._metadata_table = self._iceberg_catalog.load_table("tracking.match_metadata") + + metadata_schema = pa.schema( + [ + pa.field("match_id", pa.string(), nullable=False), + pa.field("competition_id", pa.string(), nullable=True), + pa.field("season_id", pa.string(), nullable=True), + pa.field("metadata_json", pa.string(), nullable=False), + ] + ) + + metadata_row = pa.table( + { + "match_id": [match_id], + "competition_id": [metadata.get("competition_id")], + "season_id": [metadata.get("season_id")], + "metadata_json": [json.dumps(metadata)], + }, + schema=metadata_schema, + ) + + with self._metadata_table.transaction() as txn: + txn.delete(EqualTo(term=Reference("match_id"), value=cast(Any, match_id))) # type: ignore + txn.append(metadata_row) + + def __repr__(self) -> str: + return ( + f"IcebergTrackingDataCatalog(" + f"warehouse='{self.warehouse_path}', " + f"type='{self.catalog_type}')" + ) diff --git a/python/tracking_data_container/config.py b/python/tracking_data_container/config.py deleted file mode 100644 index b7af0ce..0000000 --- a/python/tracking_data_container/config.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Configuration for TrackingDataContainer storage.""" - -from dataclasses import dataclass, field - - -@dataclass -class IcebergConfig: - """Configuration for Iceberg table optimization.""" - - # Partitioning - partition_by: list[str] = field(default_factory=lambda: ["match_id"]) - - # Parquet optimization - compression: str = "snappy" # or "zstd" - page_size: int = 1024 * 1024 # 1 MB - row_group_size: int = 128 * 1024 * 1024 # 128 MB - - # Storage features - write_page_index: bool = True - write_statistics: bool = True - use_dictionary: bool = True - - # Sorting - sort_by: list[str] = field(default_factory=lambda: ["entity_id", "timestamp"]) diff --git a/python/tracking_data_container/container.py b/python/tracking_data_container/container.py index 1a6f4d1..3e9402f 100644 --- a/python/tracking_data_container/container.py +++ b/python/tracking_data_container/container.py @@ -9,6 +9,9 @@ import polars as pl import pyarrow as pa +from ._tracking_data_container import ( + CORE_COLUMNS as _CORE_COLUMNS, +) from ._tracking_data_container import ( Expr as _Expr, ) @@ -16,15 +19,13 @@ Frame, OrientationMode, _DataField, - _LazyTrackingDataContainer, _TrackingDataContainer, ) from ._tracking_data_container import ( Selection as _Selection, ) from .coordinate_system import CoordinateSystem -from .exceptions import TrackingDataContainerError -from .schema import DataField, Fields, TrackedEntity +from .schema import DataField, Fields from .selector import ColumnSelector, Selection if TYPE_CHECKING: @@ -32,38 +33,28 @@ import pandas as pd - from .catalog import TrackingDataCatalog + from .backends.base import LazyBackend + from .catalogs.base import Catalog logger = logging.getLogger(__name__) # --- Constants for performance --- -CORE_COLUMNS = { - "match/id", - "competition/id", - "season/id", - "timestamp", - "frame_id", - "period", - "period_id", - "player/id", - "team/id", - "entity_key", -} -FLOAT_SUFFIXES = ("/x", "/y", "/z", "/dist", "/vel", "/acc", "/lat", "/long", "/minutes_played") + +CORE_COLUMNS = set(_CORE_COLUMNS) class TrackingDataContainer: """A high-performance, metadata-aware container for sports tracking data.""" _container: _TrackingDataContainer - _catalog: TrackingDataCatalog | None + _catalog: Catalog | None def __init__( self, data: dict[str, Any] | _TrackingDataContainer | pl.DataFrame | pa.Table, coordinate_system: CoordinateSystem | None = None, orientation: str | OrientationMode | None = None, - catalog: TrackingDataCatalog | None = None, + catalog: Catalog | None = None, validate: bool = True, ) -> None: self._catalog = catalog @@ -79,29 +70,9 @@ def __init__( else: data_dict = data - normalized = {} - for k, v in data_dict.items(): - if v is None or len(v) == 0: - normalized[k] = v - continue - - if k.endswith(FLOAT_SUFFIXES) or k in ("x", "y", "z", "ball_x", "ball_y", "ball_z"): - normalized[k] = [float(x) if x is not None else None for x in v] - elif k.endswith("_status") or k == "ball_status": - normalized[k] = [bool(x) if x is not None else None for x in v] - elif k in ("timestamp", "frame_id", "period_id"): - if len(v) > 0 and isinstance(v[0], datetime): - normalized[k] = [ - int(dt.timestamp() * 1_000_000) if dt is not None else None for dt in v - ] - else: - normalized[k] = [int(x) if x is not None else None for x in v] - else: - normalized[k] = [str(x) if x is not None else None for x in v] - inner_cs = coordinate_system._inner if coordinate_system else None self._container = _TrackingDataContainer( - normalized, inner_cs, str(orientation) if orientation else None + data_dict, inner_cs, str(orientation) if orientation else None ) if validate: self.validate() @@ -190,6 +161,18 @@ def add_column( self, name: str, value: Any, on: str | Sequence[str] | None = None ) -> TrackingDataContainer: if callable(value): + if on is not None: + on_resolved = [on] if isinstance(on, str) else list(on) + try: + result = value(*on_resolved) + if isinstance(result, _Expr): + new_inner = self._container.add_column(name, result) + return TrackingDataContainer( + new_inner, catalog=self._catalog, validate=False + ) + except Exception: + pass + inputs = self.select(on) if on is not None else self batch = inputs.to_arrow() args = [batch.column(c) for c in ([on] if isinstance(on, str) else list(on or []))] @@ -214,10 +197,68 @@ def add_column( ) def add_columns(self, names: Sequence[str], values: Sequence[Any]) -> TrackingDataContainer: - res = self - for n, v in zip(names, values): - res = res.add_column(n, v) - return res + expr_names = [] + expr_values = [] + array_names = [] + array_values = [] + schema_capsules = [] + + for name, value in zip(names, values): + if isinstance(value, _Expr): + expr_names.append(name) + expr_values.append(value) + elif callable(value): + # Fallback to single add_column for callables + return self.add_column(name, value) + else: + data = value + try: + cap_arr, cap_sch = data.__arrow_c_array__() + except (AttributeError, TypeError): + import pyarrow as pa + + data = pa.array(data) + cap_arr, cap_sch = data.__arrow_c_array__() + + array_names.append(name) + array_values.append(cap_arr) + schema_capsules.append(cap_sch) + + if hasattr(self._container, "clone"): + new_inner = self._container.clone() + else: + new_inner = self._container + + if expr_names or array_names: + # We call the Rust method, passing lists of Exprs or capsules. + # We must map expr_names, expr_values properly if we had a split API, + # but _TrackingDataContainer.add_columns accepts a mixed list of values! + pass + + # Since _TrackingDataContainer.add_columns takes a mixed list, let's just pass them directly + # and extract the capsules for array values. + + capsules: list[Any] = [] + final_values = [] + for v in values: + if isinstance(v, _Expr): + final_values.append(v) + capsules.append(None) + else: + try: + cap_arr, cap_sch = v.__arrow_c_array__() + except (AttributeError, TypeError): + import pyarrow as pa + + v = pa.array(v) + cap_arr, cap_sch = v.__arrow_c_array__() + final_values.append(cap_arr) + capsules.append(cap_sch) + + new_inner = self._container.add_columns( + list(names), final_values, _schema_capsules=capsules + ) + return TrackingDataContainer(new_inner, catalog=self._catalog, validate=False) def add_column_per_player( self, name: str, value: Any, on: str | Sequence[str] | None = None @@ -256,76 +297,6 @@ def to_pandas(self) -> pd.DataFrame: def to_parquet(self, path: str | Path) -> None: self._container.save_parquet(str(path)) - def melt(self) -> pl.DataFrame: - import polars as pl - - df = self.to_polars() - core = [ - c - for c in [ - "match/id", - "competition/id", - "season/id", - "timestamp", - "frame_id", - "period", - "period_id", - ] - if c in df.columns - ] - groups: dict[tuple, dict[str, str]] = {} - for c in df.columns: - if c in core: - continue - try: - f = DataField(c) - if not f.entity: - continue - - # Check for metadata columns that shouldn't be melted - if f.attribute in ("player/id", "team/id", "entity_key"): - continue - - key = ( - f.entity.type, - getattr(f.entity, "player_id", None), - getattr(f.entity, "team_id", None), - getattr(f.entity, "referee_id", None), - ) - if key not in groups: - groups[key] = {} - groups[key][f.attribute] = c - except Exception: - continue - - dfs = [] - for (et, pid, tid, rid), attrs in groups.items(): - t = df.select(core + list(attrs.values())).rename({v: k for k, v in attrs.items()}) - p_id = ( - "ball" - if et == "ball" - else ( - pid if et == "player" else (f"team:{tid}" if et == "team" else f"referee:{rid}") - ) - ) - t = t.with_columns( - [ - pl.lit(str(p_id)).cast(pl.String).alias("player/id"), - pl.lit(str(tid) if tid else None).cast(pl.String).alias("team/id"), - (pl.lit(str(p_id)) + pl.lit("|") + pl.lit(str(tid) if tid else "none")) - .cast(pl.String) - .alias("entity_key"), - ] - ) - # Ensure spatial columns are float32 for consistency - for spatial in ["x", "y", "z"]: - if spatial in t.columns: - t = t.with_columns(pl.col(spatial).cast(pl.Float32)) - - t = t.filter(~pl.all_horizontal(pl.col(list(attrs.keys())).is_null())) - dfs.append(t) - return pl.concat(dfs, how="diagonal_relaxed") if dfs else pl.DataFrame() - def __arrow_c_schema__(self) -> Any: return self._container.__arrow_c_schema__() @@ -336,7 +307,7 @@ def __arrow_c_array__(self, requested_schema: Any = None) -> Any: def from_arrow( cls, data: pa.Table | pa.RecordBatch | pa.StructArray | pa.ChunkedArray, - catalog: TrackingDataCatalog | None = None, + catalog: Catalog | None = None, ) -> TrackingDataContainer: import pyarrow as pa @@ -354,6 +325,18 @@ def from_arrow( new_inner = _TrackingDataContainer.from_arrow(cap_arr, cap_sch, None, None) return TrackingDataContainer(new_inner, catalog=catalog, validate=False) + def melt(self) -> pl.DataFrame: + """Pivots wide-format tracking data into entity-long format. + + Returns: + A long Polars DataFrame. + """ + import pyarrow as pa + + melted_stream = getattr(self._container.lazy(), "melt")() + table = pa.table(melted_stream) + return cast(pl.DataFrame, pl.from_arrow(table)) + @classmethod def pivot( cls, @@ -379,7 +362,9 @@ def pivot( return TrackingDataContainer(inner, validate=False) def lazy(self) -> LazyTrackingDataContainer: - return LazyTrackingDataContainer(self._container.lazy(), catalog=self._catalog) + from .backends.datafusion import DataFusionBackend + + return LazyTrackingDataContainer(DataFusionBackend(self._container.lazy())) def materialize_frame(self, frame: int | datetime | timedelta) -> Frame: """Materializes a single frame. Fixed datetime comparison.""" @@ -417,6 +402,162 @@ def materialize_frame(self, frame: int | datetime | timedelta) -> Frame: return self._container.materialize_frame(idx) + def plot_frame( + self, + frame: int | datetime | timedelta, + ax: Any = None, + home_color: str = "red", + away_color: str = "blue", + ball_color: str = "white", + referee_color: str = "yellow", + player_size: float = 80, + ball_size: float = 50, + **kwargs: Any, + ) -> Any: + """ + Visualizes a frame using mplsoccer. + + Parameters + ---------- + frame : int, datetime, or timedelta + The frame index, timestamp, or timedelta to plot. + ax : matplotlib.axes.Axes, optional + An existing matplotlib Axes object to plot on. + home_color : str, default="red" + Color for the home team players. + away_color : str, default="blue" + Color for the away team players. + ball_color : str, default="white" + Color for the ball. + referee_color : str, default="yellow" + Color for the referees. + player_size : float, default=0.8 + Radius of the players in pitch units. + ball_size : float, default=0.5 + Radius of the ball in pitch units. + **kwargs + Additional arguments passed to the Pitch.scatter method. + + Returns + ------- + matplotlib.axes.Axes + The matplotlib Axes object. + + Raises + ------ + ImportError + If mplsoccer is not installed. + ValueError + If the coordinate system is not defined. + """ + try: + from mplsoccer import Pitch + except ImportError: + raise ImportError( + "Seems like you don't have `mplsoccer` installed. " + "Please install it using: pip install mplsoccer" + ) + + if self.coordinate_system is None: + raise ValueError( + "Coordinate system must be defined to plot. " + "Use container.with_coordinate_system() to set it." + ) + + dot_styles = { + "home": { + "facecolor": home_color, + "edgecolor": "white", + "linewidth": 0.8, + "s": player_size, + "zorder": 5, + }, + "away": { + "facecolor": away_color, + "edgecolor": "white", + "linewidth": 0.8, + "s": player_size, + "zorder": 5, + }, + "ball": { + "facecolor": ball_color, + "edgecolor": "black", + "s": ball_size, + "zorder": 6, + }, + "referee": { + "facecolor": referee_color, + "edgecolor": "black", + "linewidth": 0.8, + "s": player_size, + "zorder": 5, + }, + } + + dims = self.coordinate_system.to_mplsoccer() + pitch = Pitch(pitch_type=dims) + + if ax is None: + fig, ax = pitch.draw() + else: + pitch.draw(ax=ax) + + f = self.materialize_frame(frame) + schema = self.schema() + + # Plot players + for tracked_team in schema.teams(): + from .schema import Team + + if not isinstance(tracked_team, Team): + continue + team_id = tracked_team.team_id + if team_id is None: + continue + style_key = "home" if "home" in team_id.lower() else "away" + style = dot_styles[style_key].copy() + style.update(kwargs) + + players = schema.players(team=team_id) + x = [] + y = [] + for p in players: + px = f[DataField(entity=p, attribute="x")] # type: ignore + py = f[DataField(entity=p, attribute="y")] # type: ignore + if px is not None and py is not None: + x.append(px) + y.append(py) + + if x: + pitch.scatter(x, y, ax=ax, label=f"Team {team_id}", **style) + + # Plot ball + from .schema import Ball + + bx = f[DataField(entity=Ball(), attribute="x")] # type: ignore + by = f[DataField(entity=Ball(), attribute="y")] # type: ignore + if bx is not None and by is not None: + style = dot_styles["ball"].copy() + style.update(kwargs) + pitch.scatter(bx, by, ax=ax, label="Ball", **style) + + # Plot referees + rx = [] + ry = [] + for ref in schema.referees(): + px = f[DataField(entity=ref, attribute="x")] # type: ignore + py = f[DataField(entity=ref, attribute="y")] # type: ignore + if px is not None and py is not None: + rx.append(px) + ry.append(py) + + if rx: + style = dot_styles["referee"].copy() + style.update(kwargs) + pitch.scatter(rx, ry, ax=ax, label="Referees", **style) + + return ax + @classmethod def from_parquet(cls, path: str | Path) -> TrackingDataContainer: """Loads from a Parquet file.""" @@ -428,248 +569,45 @@ def __repr__(self) -> str: class LazyTrackingDataContainer: - def __init__( - self, - container: _LazyTrackingDataContainer, - catalog: TrackingDataCatalog | None = None, - is_long_storage: bool = False, - deferred_select: Sequence[str] | None = None, - cached_schema: Fields | None = None, - deferred_add_columns: list[tuple] | None = None, - ): - self._container = container - self._catalog = catalog - self._is_long_storage = is_long_storage - self._deferred_select = deferred_select - self._cached_schema = cached_schema - self._deferred_add_columns = deferred_add_columns or [] + """A lazy tracking data container that delegates execution to a backend.""" + + def __init__(self, backend: LazyBackend): + self._backend = backend def schema(self) -> Fields: - """Fast reconstruction for long storage. Optimization: Selection pushdown.""" - if self._cached_schema: - return self._cached_schema - if not self._is_long_storage: - self._cached_schema = Fields(self._container.schema) - return self._cached_schema - - import polars as pl - - ids: list[str | _DataField | _Selection] = [ - _DataField.from_name("player/id"), - _DataField.from_name("team/id"), - ] - entities = self._container.select(ids).collect() - ent_df = pl.from_arrow(entities.to_arrow()) - if isinstance(ent_df, pl.Series): - ent_df = ent_df.to_frame() - ent_df = ent_df.unique() - - phys_names = [f.name() for f in self._container.schema] - phys_attrs = [n for n in phys_names if n not in CORE_COLUMNS] - - # Start with all non-entity core columns - wide_names = [c for c in CORE_COLUMNS if c not in ("player/id", "team/id", "entity_key")] - for row in ent_df.to_dicts(): - p, t = str(row["player/id"]), str(row.get("team/id", "none")) - prefix = TrackedEntity.from_entity_key(f"{p}|{t}").cdf_prefix() - for attr in phys_attrs: - wide_names.append(f"{prefix}{attr}") - - self._cached_schema = Fields(list(dict.fromkeys(wide_names))) - return self._cached_schema + return self._backend.schema() - def select(self, items: str | Sequence[str]) -> LazyTrackingDataContainer: - """Lazily selects columns. Supports selection pushdown.""" - if self._is_long_storage: - sch = self.schema() - resolved = [] - inputs = items if isinstance(items, (list, tuple)) else [items] - for it in inputs: - if isinstance(it, str): - if "*" in it: - resolved.extend(sch.match(it)) - else: - resolved.append(it) - elif isinstance(it, DataField): - resolved.append(it.name()) - - resolved = list(dict.fromkeys(resolved)) - core_set = { - "match/id", - "competition/id", - "season/id", - "timestamp", - "frame_id", - "period", - "player/id", - "team/id", - "entity_key", - } - phys_attrs, entities = set(), set() - for name in resolved: - if name in core_set: - continue - try: - f = DataField(name) - phys_attrs.add(f.attribute) - if f.entity: - if f.entity.type != "team": - pid = getattr(f.entity, "player_id", "ball") - else: - pid = f"team:{f.entity.team_id}" - entities.add((str(pid), str(getattr(f.entity, "team_id", "none")))) - except Exception: - continue + @property + def selector(self) -> ColumnSelector: + from ._tracking_data_container import ColumnSelector as _ColumnSelector - new_inner = self._container - if entities: - from ._tracking_data_container import col, lit - - pred = None - for pid, tid in entities: - p_cond = col("player/id").eq(lit(pid)) - if tid != "none": - p_cond = p_cond & col("team/id").eq(lit(tid)) - pred = p_cond if pred is None else (pred | p_cond) - if pred: - new_inner = new_inner.filter(pred) - - phys_names = [f.name() for f in self._container.schema] - phys_sel = [n for n in core_set if n in phys_names] - phys_sel.extend([n for n in phys_attrs if n in phys_names]) - - rust_sel: list[str | _DataField | _Selection] = [ - _DataField.from_name(n) for n in phys_sel - ] - return LazyTrackingDataContainer( - new_inner.select(rust_sel), - catalog=self._catalog, - is_long_storage=True, - deferred_select=resolved, - cached_schema=Fields(resolved), - deferred_add_columns=self._deferred_add_columns, - ) + return ColumnSelector(_ColumnSelector()) - inputs = items if isinstance(items, (list, tuple)) else [items] - processed: list[str | _DataField | _Selection] = [] - for i in inputs: - if isinstance(i, str): - if "*" in i: - for m in self.schema().match(i): - processed.append(_DataField.from_name(m)) - else: - processed.append(_DataField.from_name(i)) - elif isinstance(i, DataField): - processed.append(i._field) - elif isinstance(i, Selection): - processed.append(i._inner) - elif isinstance(i, (_DataField, _Selection)): - processed.append(i) - - new_cache = None - if self._cached_schema: - names = [] - for itm in inputs: - if isinstance(itm, str): - names.extend(self._cached_schema.match(itm)) - else: - names.append(str(itm)) - new_cache = Fields(list(dict.fromkeys(names))) - - return LazyTrackingDataContainer( - self._container.select(processed), - catalog=self._catalog, - cached_schema=new_cache, - deferred_add_columns=self._deferred_add_columns, - ) + def select(self, items: str | Sequence[str]) -> LazyTrackingDataContainer: + return LazyTrackingDataContainer(self._backend.select(items)) def add_column( self, name: str, value: Any, on: str | Sequence[str] | None = None ) -> LazyTrackingDataContainer: - if isinstance(value, _Expr): - return LazyTrackingDataContainer( - self._container.add_column(name, value), - catalog=self._catalog, - is_long_storage=self._is_long_storage, - deferred_select=self._deferred_select, - cached_schema=self._cached_schema, - deferred_add_columns=self._deferred_add_columns, - ) - - if callable(value): - new_deferred = list(self._deferred_add_columns) - new_deferred.append((name, value, on)) - return LazyTrackingDataContainer( - self._container, - catalog=self._catalog, - is_long_storage=self._is_long_storage, - deferred_select=self._deferred_select, - cached_schema=self._cached_schema, - deferred_add_columns=new_deferred, - ) - - raise TrackingDataContainerError( - "Adding non-expression columns to lazy containers is not yet supported." - ) + return LazyTrackingDataContainer(self._backend.add_column(name, value, on)) def collect(self) -> TrackingDataContainer: - """Executes the plan and pivots if necessary.""" - try: - inner = self._container.collect() - res = TrackingDataContainer(inner, catalog=self._catalog, validate=False) - - for name, value, on in self._deferred_add_columns: - res = res.add_column(name, value, on=on) - - if self._is_long_storage: - res = TrackingDataContainer.pivot(res.to_arrow()) - res._catalog = self._catalog - if self._deferred_select: - logger.debug(f"Wide container columns before selection: {res.schema().names}") - res = res.select(list(self._deferred_select)) - logger.debug(f"Wide container columns after selection: {res.schema().names}") - return res - except Exception as e: - raise TrackingDataContainerError(f"Collection failed: {e}") + return self._backend.collect() def slice(self, offset: int, length: int) -> LazyTrackingDataContainer: - return LazyTrackingDataContainer( - self._container.slice(offset, length), - catalog=self._catalog, - is_long_storage=self._is_long_storage, - cached_schema=self._cached_schema, - deferred_add_columns=self._deferred_add_columns, - ) + return LazyTrackingDataContainer(self._backend.slice(offset, length)) def filter(self, predicate: _Expr) -> LazyTrackingDataContainer: - return LazyTrackingDataContainer( - self._container.filter(predicate), - catalog=self._catalog, - is_long_storage=self._is_long_storage, - cached_schema=self._cached_schema, - deferred_add_columns=self._deferred_add_columns, - ) + return LazyTrackingDataContainer(self._backend.filter(predicate)) def transform_coordinates(self, sys: CoordinateSystem) -> LazyTrackingDataContainer: - return LazyTrackingDataContainer( - self._container.transform_coordinates(sys._inner), - catalog=self._catalog, - is_long_storage=self._is_long_storage, - cached_schema=self._cached_schema, - deferred_add_columns=self._deferred_add_columns, - ) + return LazyTrackingDataContainer(self._backend.transform_coordinates(sys)) def transform_orientation(self, mode: str | OrientationMode) -> LazyTrackingDataContainer: - return LazyTrackingDataContainer( - self._container.transform_orientation(str(mode)), - catalog=self._catalog, - is_long_storage=self._is_long_storage, - cached_schema=self._cached_schema, - deferred_add_columns=self._deferred_add_columns, - ) + return LazyTrackingDataContainer(self._backend.transform_orientation(mode)) def __arrow_c_stream__(self, rs: Any = None) -> Any: - return self._container.__arrow_c_stream__(rs) + return self._backend.__arrow_c_stream__(rs) def __repr__(self) -> str: - return repr(self._container) + return f"LazyTrackingDataContainer(backend={self._backend.__class__.__name__})" diff --git a/python/tracking_data_container/io.py b/python/tracking_data_container/io.py index 36aa3e2..7391a3f 100644 --- a/python/tracking_data_container/io.py +++ b/python/tracking_data_container/io.py @@ -38,8 +38,10 @@ def scan_parquet(path: str | Path) -> LazyTrackingDataContainer: LazyTrackingDataContainer A lazy container representing the Parquet file. """ + from .backends.datafusion import DataFusionBackend + new_inner = _scan_parquet(str(path)) - return LazyTrackingDataContainer(new_inner) + return LazyTrackingDataContainer(DataFusionBackend(new_inner)) def load_sportec(path: str | Path, meta_path: str | Path | None = None) -> TrackingDataContainer: diff --git a/python/tracking_data_container/schema.py b/python/tracking_data_container/schema.py index 01351d9..cd133b7 100644 --- a/python/tracking_data_container/schema.py +++ b/python/tracking_data_container/schema.py @@ -129,6 +129,9 @@ def _from_inner(entity: _TrackedEntity) -> TrackedEntity: def __repr__(self) -> str: return repr(self._entity) + def __hash__(self) -> int: + return hash(self.entity_key()) + class Player(TrackedEntity): def __init__(self, team_id: str, player_id: str) -> None: @@ -148,6 +151,8 @@ def __eq__(self, other: object) -> bool: return False return self.team_id == other.team_id and self.player_id == other.player_id + __hash__ = TrackedEntity.__hash__ + class Ball(TrackedEntity): def __init__(self) -> None: @@ -165,6 +170,8 @@ def __repr__(self) -> str: def __eq__(self, other: object) -> bool: return isinstance(other, Ball) + __hash__ = TrackedEntity.__hash__ + class Team(TrackedEntity): def __init__(self, team_id: str) -> None: @@ -184,6 +191,8 @@ def __eq__(self, other: object) -> bool: return False return self.team_id == other.team_id + __hash__ = TrackedEntity.__hash__ + class Referee(TrackedEntity): def __init__(self, referee_id: str) -> None: @@ -203,6 +212,8 @@ def __eq__(self, other: object) -> bool: return False return self.referee_id == other.referee_id + __hash__ = TrackedEntity.__hash__ + class DataField: """Describes a single column/field in the tracking data.""" @@ -279,6 +290,10 @@ def __eq__(self, other: object) -> bool: return False return self.name() == other.name() + def to_iceberg_name(self) -> str: + """Returns the mapped physical column name for Iceberg long storage format.""" + return str(getattr(self._field, "to_iceberg_name")()) + def __repr__(self) -> str: return f"DataField({self.name()})" @@ -400,10 +415,12 @@ def validate(self) -> None: warnings.warn(" AND ".join(all_warnings), SchemaWarning) def validate_quiet(self) -> bool: + from .exceptions import SchemaError + try: self.validate() return True - except Exception: + except SchemaError: return False def __repr__(self) -> str: From 33963ce7da33df6c5928045d6697cbb8b44a6dec Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Wed, 29 Jul 2026 16:04:45 +0200 Subject: [PATCH 21/21] feat: support multiple schemas --- crates/tdc-core/src/datacontainer/core.rs | 102 +++++- crates/tdc-core/src/datacontainer/lazy.rs | 18 +- crates/tdc-core/src/io.rs | 15 +- crates/tdc-core/src/lib.rs | 3 +- crates/tdc-core/src/schema/definition.rs | 213 ++++++++++++ crates/tdc-core/src/schema/fields.rs | 128 +++---- crates/tdc-core/src/schema/mod.rs | 4 + crates/tdc-core/src/schema/registry.rs | 314 ++++++++++++++++++ crates/tdc-parsers/src/hawkeye.rs | 13 +- crates/tdc-parsers/src/sportec.rs | 49 ++- crates/tdc-parsers/src/statsperform.rs | 9 +- crates/tdc-parsers/tests/test_sportec.rs | 3 + crates/tdc-python/src/container.rs | 28 +- crates/tdc-python/src/schema.rs | 78 +++++ examples/01-basics.ipynb | 82 ++++- examples/02-parsing.ipynb | 18 +- examples/04-udfs.ipynb | 12 +- examples/05-lazy.ipynb | 2 +- examples/06-skeletal.ipynb | 56 ++-- examples/08-idsse-warehouse.ipynb | 111 ++----- python/tests/test_schema.py | 4 +- python/tracking_data_container/__init__.py | 4 + .../_tracking_data_container.pyi | 23 ++ .../backends/iceberg.py | 6 + .../catalogs/iceberg.py | 2 + python/tracking_data_container/container.py | 29 +- python/tracking_data_container/schema.py | 166 ++++++--- 27 files changed, 1219 insertions(+), 273 deletions(-) create mode 100644 crates/tdc-core/src/schema/definition.rs create mode 100644 crates/tdc-core/src/schema/registry.rs diff --git a/crates/tdc-core/src/datacontainer/core.rs b/crates/tdc-core/src/datacontainer/core.rs index 539c09a..91f7a9a 100644 --- a/crates/tdc-core/src/datacontainer/core.rs +++ b/crates/tdc-core/src/datacontainer/core.rs @@ -1,5 +1,7 @@ use crate::error::{TrackingDataContainerErrorKind, TrackingDataContainerResult}; -use crate::schema::{CoordinateSystem, DataField, Fields, OrientationMode, TrackedEntity}; +use crate::schema::{ + CoordinateSystem, DataField, Fields, OrientationMode, SchemaId, TrackedEntity, +}; use arrow_arith::numeric::{add, mul}; use arrow_array::array::{Array, ArrayRef, Float32Array, Float64Array, StructArray}; use arrow_array::cast::AsArray; @@ -16,6 +18,7 @@ pub struct TrackingDataContainer { pub schema: Fields, pub coordinate_system: Option, pub orientation: Option, + pub schema_id: Option, } impl Clone for TrackingDataContainer { @@ -25,6 +28,7 @@ impl Clone for TrackingDataContainer { schema: self.schema.clone(), coordinate_system: self.coordinate_system.clone(), orientation: self.orientation, + schema_id: self.schema_id.clone(), } } } @@ -36,6 +40,7 @@ impl TrackingDataContainer { schema, coordinate_system: None, orientation: None, + schema_id: None, } } @@ -49,6 +54,11 @@ impl TrackingDataContainer { self } + pub fn with_schema_id(mut self, id: SchemaId) -> Self { + self.schema_id = Some(id); + self + } + pub fn num_rows(&self) -> usize { self.data.len() } @@ -62,11 +72,94 @@ impl TrackingDataContainer { schema: self.schema.clone(), coordinate_system: self.coordinate_system.clone(), orientation: self.orientation, + schema_id: self.schema_id.clone(), } } pub fn validate(&self) -> TrackingDataContainerResult<()> { - self.schema.validate() + match &self.schema_id { + Some(id) => self.schema.validate_with_schema(id)?, + None => self.schema.validate()?, + } + + let default_id = crate::schema::registry::default_schema_id(); + let schema_id = self.schema_id.as_ref().unwrap_or(&default_id); + + if let Some(schema_def) = crate::schema::registry::get_schema(schema_id) { + for req in &schema_def.value_requirements { + if let Some(idx) = self + .schema + .fields + .iter() + .position(|f| f.name() == req.column) + { + let col = self.data.column(idx); + if let Some(string_col) = col.as_any().downcast_ref::() { + for i in 0..string_col.len() { + if !string_col.is_null(i) { + let val = string_col.value(i); + if !req.allowed_values.iter().any(|v| v == val) { + return Err(crate::error::TrackingDataContainerError::new( + TrackingDataContainerErrorKind::SchemaError(format!( + "Invalid value for column '{}': {}", + req.column, val + )), + )); + } + } + } + } else if let Some(large_string_col) = col.as_any().downcast_ref::() { + for i in 0..large_string_col.len() { + if !large_string_col.is_null(i) { + let val = large_string_col.value(i); + if !req.allowed_values.iter().any(|v| v == val) { + return Err(crate::error::TrackingDataContainerError::new( + TrackingDataContainerErrorKind::SchemaError(format!( + "Invalid value for column '{}': {}", + req.column, val + )), + )); + } + } + } + } else if let Some(dict_col) = col.as_any().downcast_ref::>() { + let values = dict_col.values(); + if let Some(string_values) = values.as_any().downcast_ref::() { + for i in 0..dict_col.len() { + if !dict_col.is_null(i) { + let key = dict_col.keys().value(i); + let val = string_values.value(key as usize); + if !req.allowed_values.iter().any(|v| v == val) { + return Err(crate::error::TrackingDataContainerError::new( + TrackingDataContainerErrorKind::SchemaError(format!( + "Invalid value for column '{}': {}", + req.column, val + )), + )); + } + } + } + } else if let Some(large_string_values) = values.as_any().downcast_ref::() { + for i in 0..dict_col.len() { + if !dict_col.is_null(i) { + let key = dict_col.keys().value(i); + let val = large_string_values.value(key as usize); + if !req.allowed_values.iter().any(|v| v == val) { + return Err(crate::error::TrackingDataContainerError::new( + TrackingDataContainerErrorKind::SchemaError(format!( + "Invalid value for column '{}': {}", + req.column, val + )), + )); + } + } + } + } + } + } + } + } + Ok(()) } pub fn lazy(&self) -> TrackingDataContainerResult { @@ -77,6 +170,7 @@ impl TrackingDataContainer { self.schema.clone(), self.coordinate_system.clone(), self.orientation, + self.schema_id.clone(), )) } @@ -111,6 +205,7 @@ impl TrackingDataContainer { }, coordinate_system: self.coordinate_system.clone(), orientation: self.orientation, + schema_id: self.schema_id.clone(), } } @@ -221,6 +316,7 @@ impl TrackingDataContainer { schema: self.schema.clone(), coordinate_system: Some(to_system), orientation: self.orientation, + schema_id: self.schema_id.clone(), }) } @@ -333,6 +429,7 @@ impl TrackingDataContainer { schema: self.schema.clone(), coordinate_system: self.coordinate_system.clone(), orientation: Some(mode), + schema_id: self.schema_id.clone(), }) } @@ -768,6 +865,7 @@ impl crate::selector::Select for TrackingDataContainer { }, coordinate_system: self.coordinate_system.clone(), orientation: self.orientation, + schema_id: self.schema_id.clone(), }) } } diff --git a/crates/tdc-core/src/datacontainer/lazy.rs b/crates/tdc-core/src/datacontainer/lazy.rs index f98f2bc..0aeca41 100644 --- a/crates/tdc-core/src/datacontainer/lazy.rs +++ b/crates/tdc-core/src/datacontainer/lazy.rs @@ -1,6 +1,6 @@ use crate::datacontainer::TrackingDataContainer; use crate::error::TrackingDataContainerResult; -use crate::schema::{CoordinateSystem, DataField, Fields, OrientationMode}; +use crate::schema::{CoordinateSystem, DataField, Fields, OrientationMode, SchemaId}; use datafusion::prelude::*; #[derive(Clone)] @@ -9,6 +9,7 @@ pub struct LazyTrackingDataContainer { pub schema: Fields, pub coordinate_system: Option, pub orientation: Option, + pub schema_id: Option, } impl crate::selector::expr::HasSchema for LazyTrackingDataContainer { @@ -30,6 +31,7 @@ impl crate::selector::Select for LazyTrackingDataContainer { schema: Fields { fields }, coordinate_system: self.coordinate_system.clone(), orientation: self.orientation, + schema_id: self.schema_id.clone(), }) } } @@ -40,12 +42,14 @@ impl LazyTrackingDataContainer { schema: Fields, coordinate_system: Option, orientation: Option, + schema_id: Option, ) -> Self { Self { df, schema, coordinate_system, orientation, + schema_id, } } @@ -61,6 +65,7 @@ impl LazyTrackingDataContainer { schema: self.schema.clone(), coordinate_system: self.coordinate_system.clone(), orientation: self.orientation, + schema_id: self.schema_id.clone(), }) } @@ -74,6 +79,7 @@ impl LazyTrackingDataContainer { schema: self.schema.clone(), coordinate_system: self.coordinate_system.clone(), orientation: self.orientation, + schema_id: self.schema_id.clone(), }) } @@ -108,6 +114,7 @@ impl LazyTrackingDataContainer { schema: self.schema.clone(), coordinate_system: Some(to_system), orientation: self.orientation, + schema_id: self.schema_id.clone(), }) } @@ -198,6 +205,7 @@ impl LazyTrackingDataContainer { schema: self.schema.clone(), coordinate_system: self.coordinate_system.clone(), orientation: Some(mode), + schema_id: self.schema_id.clone(), }) } @@ -260,6 +268,7 @@ impl LazyTrackingDataContainer { }, coordinate_system: self.coordinate_system.clone(), orientation: self.orientation, + schema_id: self.schema_id.clone(), }) } @@ -465,6 +474,7 @@ impl LazyTrackingDataContainer { }, coordinate_system: self.coordinate_system.clone(), orientation: self.orientation, + schema_id: self.schema_id.clone(), }) } @@ -481,6 +491,9 @@ impl LazyTrackingDataContainer { if let Some(om) = self.orientation { container = container.with_orientation(om); } + if let Some(id) = self.schema_id { + container = container.with_schema_id(id); + } return Ok(container); } @@ -498,6 +511,9 @@ impl LazyTrackingDataContainer { if let Some(om) = self.orientation { container = container.with_orientation(om); } + if let Some(id) = self.schema_id { + container = container.with_schema_id(id); + } Ok(container) } diff --git a/crates/tdc-core/src/io.rs b/crates/tdc-core/src/io.rs index 723946b..096a7f4 100644 --- a/crates/tdc-core/src/io.rs +++ b/crates/tdc-core/src/io.rs @@ -1,6 +1,6 @@ use crate::datacontainer::TrackingDataContainer; use crate::error::TrackingDataContainerResult; -use crate::schema::Fields; +use crate::schema::{Fields, SchemaId}; use arrow_array::RecordBatch; use parquet::arrow::ArrowWriter; use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder; @@ -31,6 +31,13 @@ impl TrackingDataContainer { )); } + if let Some(id) = &self.schema_id { + key_value_metadata.push(parquet::format::KeyValue::new( + "tdc:schema".to_string(), + Some(id.to_string()), + )); + } + let props_builder = WriterProperties::builder().set_key_value_metadata(Some(key_value_metadata)); @@ -48,6 +55,7 @@ impl TrackingDataContainer { let mut coordinate_system = None; let mut orientation = None; + let mut schema_id = None; if let Some(metadata) = builder.metadata().file_metadata().key_value_metadata() { for kv in metadata { @@ -60,6 +68,10 @@ impl TrackingDataContainer { && let Some(val) = &kv.value { orientation = Some(serde_json::from_str(val).map_err(|e| e.to_string())?); + } else if kv.key == "tdc:schema" + && let Some(val) = &kv.value + { + schema_id = SchemaId::parse(val); } } } @@ -85,6 +97,7 @@ impl TrackingDataContainer { let mut container = Self::new(batch.into(), schema); container.coordinate_system = coordinate_system; container.orientation = orientation; + container.schema_id = schema_id; Ok(container) } diff --git a/crates/tdc-core/src/lib.rs b/crates/tdc-core/src/lib.rs index 3ddd353..012aa60 100644 --- a/crates/tdc-core/src/lib.rs +++ b/crates/tdc-core/src/lib.rs @@ -8,6 +8,7 @@ pub mod selector; pub use datacontainer::{LazyTrackingDataContainer, TrackingDataBuilder, TrackingDataContainer}; pub use schema::{ Attribute, CoordinateSystem, DataField, Direction, Fields, FrameAttribute, LengthUnit, - OrientationMode, Origin, Period, TrackedEntity, TrackedEntityAttribute, + OrientationMode, Origin, Period, SchemaDefinition, SchemaId, TrackedEntity, + TrackedEntityAttribute, default_schema_id, get_schema, list_schemas, }; pub use selector::Select; diff --git a/crates/tdc-core/src/schema/definition.rs b/crates/tdc-core/src/schema/definition.rs new file mode 100644 index 0000000..af8be33 --- /dev/null +++ b/crates/tdc-core/src/schema/definition.rs @@ -0,0 +1,213 @@ +//! Schema identification and definition types. +//! +//! This module provides the core types for multi-schema support, allowing +//! TDC to validate data against different schema definitions (e.g., 2D tracking, +//! skeletal tracking, GPS data) for different sports and versions. + +use super::{Attribute, FrameAttribute, TrackedEntityAttribute}; +use crate::error::{ + TrackingDataContainerError, TrackingDataContainerErrorKind, TrackingDataContainerResult, +}; +use std::fmt; + +/// Identifies a specific schema by sport, data kind, and version. +/// +/// # Examples +/// +/// - CDF tracking: `SchemaId::new("cdf", "tracking", "0.2.3")` +/// - CDF skeletal: `SchemaId::new("cdf", "skeletal", "0.2.3")` +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct SchemaId { + /// The sport or standard this schema applies to (e.g., "cdf", "football"). + pub sport: String, + /// The kind of data (e.g., "tracking", "skeletal"). + pub data_kind: String, + /// The version string (e.g., "0.2.2"). + pub version: String, +} + +impl SchemaId { + /// Creates a new schema identifier. + pub fn new( + sport: impl Into, + data_kind: impl Into, + version: impl Into, + ) -> Self { + Self { + sport: sport.into(), + data_kind: data_kind.into(), + version: version.into(), + } + } + + /// Parses a schema ID from a slash-separated string like "football/tracking_2d/0.2.2". + pub fn parse(s: &str) -> Option { + let parts: Vec<&str> = s.split('/').collect(); + if parts.len() == 3 { + Some(Self::new(parts[0], parts[1], parts[2])) + } else { + None + } + } +} + +impl fmt::Display for SchemaId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}/{}/{}", self.sport, self.data_kind, self.version) + } +} + +/// Defines what entities a schema requires. +#[derive(Clone)] +pub struct EntityRequirement { + /// The validation function that checks if this requirement is met. + pub check: fn(&super::Fields) -> bool, + /// The error message when this requirement is not met. + pub error_field: String, +} + +impl fmt::Debug for EntityRequirement { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("EntityRequirement") + .field("error_field", &self.error_field) + .finish() + } +} + +/// Defines allowed values for a specific column. +#[derive(Clone)] +pub struct ValueRequirement { + /// The column name to check. + pub column: String, + /// The list of allowed values. + pub allowed_values: Vec, +} + +impl fmt::Debug for ValueRequirement { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ValueRequirement") + .field("column", &self.column) + .field("allowed_values", &self.allowed_values) + .finish() + } +} + +/// A schema definition containing validation rules for a specific data format. +/// +/// Each schema defines which frame attributes, ball attributes, and entity structures +/// are required for valid data. +#[derive(Clone)] +pub struct SchemaDefinition { + /// The unique schema identifier. + pub id: SchemaId, + /// Human-readable description of the schema. + pub description: String, + /// Required frame-level attributes (e.g., FrameId, Timestamp). + pub required_frame_attributes: Vec, + /// Required ball attributes (empty if no ball in this schema). + pub required_ball_attributes: Vec, + /// Structural entity requirements (e.g., "at least one team with players"). + pub entity_requirements: Vec, + /// Value constraints for specific columns. + pub value_requirements: Vec, + /// Core column names for this schema (used in pivot/melt operations). + pub core_columns: Vec, +} + +impl SchemaDefinition { + /// Validates a `Fields` collection against this schema's requirements. + /// + /// Returns `Ok(())` if all required fields are present, or an error + /// describing which fields are missing. + pub fn validate(&self, fields: &super::Fields) -> TrackingDataContainerResult<()> { + let mut missing = Vec::new(); + + // 1. Check required frame attributes + for attr in &self.required_frame_attributes { + if !fields.has_frame_attr(attr.clone()) { + let name = match attr { + FrameAttribute::MatchId => "match/id".to_string(), + _ => attr.name().to_string(), + }; + missing.push(name); + } + } + + // 2. Check required ball attributes + for attr in &self.required_ball_attributes { + if !fields.has_ball_attr(attr.clone()) { + missing.push(format!("ball/{}", attr.name())); + } + } + + // 3. Check entity requirements + for req in &self.entity_requirements { + if !(req.check)(fields) { + missing.push(req.error_field.clone()); + } + } + + if !missing.is_empty() { + return Err(TrackingDataContainerError::new( + TrackingDataContainerErrorKind::SchemaError(format!( + "Schema '{}' validation failed. Missing required fields: {}", + self.id, + missing.join(", ") + )), + )); + } + + // 4. Check for unsupported fields (warning) + fields.check_unsupported_fields() + } +} + +impl fmt::Debug for SchemaDefinition { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SchemaDefinition") + .field("id", &self.id) + .field("description", &self.description) + .finish() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_schema_id_new() { + let id = SchemaId::new("football", "tracking_2d", "0.2.2"); + assert_eq!(id.sport, "football"); + assert_eq!(id.data_kind, "tracking_2d"); + assert_eq!(id.version, "0.2.2"); + } + + #[test] + fn test_schema_id_display() { + let id = SchemaId::new("football", "tracking_2d", "0.2.2"); + assert_eq!(id.to_string(), "football/tracking_2d/0.2.2"); + } + + #[test] + fn test_schema_id_parse() { + let id = SchemaId::parse("football/tracking_2d/0.2.2").unwrap(); + assert_eq!(id, SchemaId::new("football", "tracking_2d", "0.2.2")); + } + + #[test] + fn test_schema_id_parse_invalid() { + assert!(SchemaId::parse("football/tracking_2d").is_none()); + assert!(SchemaId::parse("football").is_none()); + assert!(SchemaId::parse("").is_none()); + } + + #[test] + fn test_schema_id_equality() { + let a = SchemaId::new("football", "tracking_2d", "0.2.2"); + let b = SchemaId::new("football", "tracking_2d", "0.2.2"); + let c = SchemaId::new("football", "skeletal", "0.1.0"); + assert_eq!(a, b); + assert_ne!(a, c); + } +} diff --git a/crates/tdc-core/src/schema/fields.rs b/crates/tdc-core/src/schema/fields.rs index a883f94..bcd8f18 100644 --- a/crates/tdc-core/src/schema/fields.rs +++ b/crates/tdc-core/src/schema/fields.rs @@ -189,79 +189,42 @@ impl Fields { result } - /// Validates the fields against the **Common Data Format Tracking Data Schema (Version 0.2.2)**. + /// Validates the fields against the default schema (CDF v0.2.3 tracking). /// - /// This method checks for the presence of mandatory fields such as `frame_id`, `ball/x/y/z`, - /// and team identifiers. It also ensures that both the home and away teams have at least - /// one player with required coordinates. + /// This is a convenience method that delegates to [`validate_with_schema`] using + /// the default schema ID. /// /// # Errors /// /// Returns a [`TrackingDataContainerError`] of kind `SchemaError` if required fields are missing. pub fn validate(&self) -> TrackingDataContainerResult<()> { - let mut missing = Vec::new(); - - // 1. Required top-level fields - if !self.has_frame_attr(FrameAttribute::FrameId) { - missing.push("frame_id"); - } - if !self.has_frame_attr(FrameAttribute::Timestamp) { - missing.push("timestamp"); - } - if !self.has_frame_attr(FrameAttribute::Period) { - missing.push("period"); - } - if !self.has_frame_attr(FrameAttribute::MatchId) { - missing.push("match/id"); - } - - // 2. Required ball fields - if !self.has_ball_attr(TrackedEntityAttribute::X) { - missing.push("ball/x"); - } - if !self.has_ball_attr(TrackedEntityAttribute::Y) { - missing.push("ball/y"); - } - if !self.has_ball_attr(TrackedEntityAttribute::Z) { - missing.push("ball/z"); - } - - // 3. Required team fields - let home_exists = self.has_team("home"); - let away_exists = self.has_team("away"); - - if !home_exists { - missing.push("teams/home"); - } else { - if !self.has_team_attr("home", TrackedEntityAttribute::Id) { - missing.push("teams/home/id"); - } - if !self.has_any_player("home") { - missing.push("teams/home/players"); - } - } - - if !away_exists { - missing.push("teams/away"); - } else { - if !self.has_team_attr("away", TrackedEntityAttribute::Id) { - missing.push("teams/away/id"); - } - if !self.has_any_player("away") { - missing.push("teams/away/players"); - } - } + let default_id = super::registry::default_schema_id(); + self.validate_with_schema(&default_id) + } - if !missing.is_empty() { - return Err(TrackingDataContainerError::new( - TrackingDataContainerErrorKind::SchemaError(format!( - "Missing required fields: {}", - missing.join(", ") - )), - )); - } + /// Validates the fields against a specific schema definition. + /// + /// Looks up the schema in the global registry and checks all required fields. + /// + /// # Errors + /// + /// Returns a [`TrackingDataContainerError`] of kind `SchemaError` if the schema is unknown + /// or if required fields are missing. + pub fn validate_with_schema( + &self, + schema_id: &super::definition::SchemaId, + ) -> TrackingDataContainerResult<()> { + let schema_def = super::registry::get_schema(schema_id).ok_or_else(|| { + TrackingDataContainerError::new(TrackingDataContainerErrorKind::SchemaError(format!( + "Unknown schema: {}", + schema_id + ))) + })?; + schema_def.validate(self) + } - // 4. Check for unsupported fields + /// Checks for unsupported fields (Other variants) and emits a warning. + pub(crate) fn check_unsupported_fields(&self) -> TrackingDataContainerResult<()> { let mut unsupported = Vec::new(); for field in &self.fields { match field { @@ -292,20 +255,20 @@ impl Fields { Ok(()) } - /// Internal helper to check for a specific global frame attribute. - fn has_frame_attr(&self, attr: FrameAttribute) -> bool { + /// Helper to check for a specific global frame attribute. + pub(crate) fn has_frame_attr(&self, attr: FrameAttribute) -> bool { self.fields .iter() .any(|f| matches!(f, DataField::Frame { attribute } if *attribute == attr)) } - /// Internal helper to check for a specific ball attribute. - fn has_ball_attr(&self, attr: TrackedEntityAttribute) -> bool { + /// Helper to check for a specific ball attribute. + pub(crate) fn has_ball_attr(&self, attr: TrackedEntityAttribute) -> bool { self.fields.iter().any(|f| matches!(f, DataField::TrackedEntity { entity: TrackedEntity::Ball, attribute } if *attribute == attr)) } - /// Internal helper to check if any field belongs to a specific team (either team-level or player-level). - fn has_team(&self, tid: &str) -> bool { + /// Helper to check if any field belongs to a specific team (either team-level or player-level). + pub(crate) fn has_team(&self, tid: &str) -> bool { self.fields.iter().any(|f| match f { DataField::TrackedEntity { entity: TrackedEntity::Team { team_id }, @@ -319,13 +282,13 @@ impl Fields { }) } - /// Internal helper to check for a specific team-level attribute. - fn has_team_attr(&self, tid: &str, attr: TrackedEntityAttribute) -> bool { + /// Helper to check for a specific team-level attribute. + pub(crate) fn has_team_attr(&self, tid: &str, attr: TrackedEntityAttribute) -> bool { self.fields.iter().any(|f| matches!(f, DataField::TrackedEntity { entity: TrackedEntity::Team { team_id }, attribute } if team_id == tid && *attribute == attr)) } - /// Internal helper to verify that at least one player for a team has required spatial data. - fn has_any_player(&self, tid: &str) -> bool { + /// Helper to verify that at least one player for a team has required spatial data. + pub(crate) fn has_any_player(&self, tid: &str) -> bool { let mut players_with_x = std::collections::HashSet::new(); let mut players_with_y = std::collections::HashSet::new(); @@ -352,4 +315,19 @@ impl Fields { .next() .is_some() } + + /// Returns true if any player has landmark data. + pub(crate) fn has_any_landmark(&self) -> bool { + self.fields.iter().any(|f| { + matches!( + f, + DataField::TrackedEntity { + attribute: TrackedEntityAttribute::LandmarkX(_) + | TrackedEntityAttribute::LandmarkY(_) + | TrackedEntityAttribute::LandmarkZ(_), + .. + } + ) + }) + } } diff --git a/crates/tdc-core/src/schema/mod.rs b/crates/tdc-core/src/schema/mod.rs index 83a5928..8ff14c9 100644 --- a/crates/tdc-core/src/schema/mod.rs +++ b/crates/tdc-core/src/schema/mod.rs @@ -1,13 +1,17 @@ pub mod attribute; pub mod coordinate_system; +pub mod definition; pub mod entity; pub mod field; pub mod fields; pub mod period; +pub mod registry; pub use attribute::{Attribute, FrameAttribute, TrackedEntityAttribute}; pub use coordinate_system::{CoordinateSystem, Direction, LengthUnit, OrientationMode, Origin}; +pub use definition::{SchemaDefinition, SchemaId}; pub use entity::TrackedEntity; pub use field::DataField; pub use fields::Fields; pub use period::Period; +pub use registry::{default_schema_id, get_schema, list_schemas}; diff --git a/crates/tdc-core/src/schema/registry.rs b/crates/tdc-core/src/schema/registry.rs new file mode 100644 index 0000000..9647670 --- /dev/null +++ b/crates/tdc-core/src/schema/registry.rs @@ -0,0 +1,314 @@ +//! Compile-time registry of known schema definitions. +//! +//! This module provides a global registry of all built-in schema definitions, +//! indexed by their `SchemaId`. New schemas can be added by defining them here. +//! Current schemas are based on the [CDF v0.2.3 specification](https://arxiv.org/pdf/2505.15820). + +use super::definition::{EntityRequirement, SchemaDefinition, SchemaId, ValueRequirement}; +use super::{FrameAttribute, TrackedEntityAttribute}; +use once_cell::sync::Lazy; +use std::collections::HashMap; + +/// Global registry of all known schema definitions. +static REGISTRY: Lazy> = Lazy::new(|| { + let mut m = HashMap::new(); + + let tracking = cdf_tracking_v0_2_3(); + m.insert(tracking.id.clone(), tracking); + + let skeletal = cdf_skeletal_v0_2_3(); + m.insert(skeletal.id.clone(), skeletal); + + m +}); + +/// Looks up a schema definition by ID. +pub fn get_schema(id: &SchemaId) -> Option<&'static SchemaDefinition> { + REGISTRY.get(id) +} + +/// Returns all registered schema IDs. +pub fn list_schemas() -> Vec<&'static SchemaId> { + REGISTRY.keys().collect() +} + +/// The default schema ID: CDF v0.2.3 tracking data. +pub fn default_schema_id() -> SchemaId { + SchemaId::new("cdf", "tracking", "0.2.3") +} + +// --------------------------------------------------------------------------- +// Built-in schema definitions +// --------------------------------------------------------------------------- + +/// CDF v0.2.3 tracking data. +/// +/// Based on +/// +/// Required top-level: frame_id, timestamp, period, match (with id), teams (home & away), ball (x, y, z). +/// Each team requires: id, players (each with id, x, y). +pub fn cdf_tracking_v0_2_3() -> SchemaDefinition { + SchemaDefinition { + id: SchemaId::new("cdf", "tracking", "0.2.3"), + description: "CDF v0.2.3 tracking data".to_string(), + required_frame_attributes: vec![ + FrameAttribute::FrameId, + FrameAttribute::Timestamp, + FrameAttribute::Period, + FrameAttribute::MatchId, + ], + required_ball_attributes: vec![ + TrackedEntityAttribute::X, + TrackedEntityAttribute::Y, + TrackedEntityAttribute::Z, + ], + entity_requirements: vec![ + // teams.home required + EntityRequirement { + check: |f| f.has_team("home"), + error_field: "teams/home".to_string(), + }, + // teams.home.id required + EntityRequirement { + check: |f| { + if f.has_team("home") { + f.has_team_attr("home", TrackedEntityAttribute::Id) + } else { + true + } + }, + error_field: "teams/home/id".to_string(), + }, + // teams.home.players required (at least one) + EntityRequirement { + check: |f| { + if f.has_team("home") { + f.has_any_player("home") + } else { + true + } + }, + error_field: "teams/home/players".to_string(), + }, + // teams.away required + EntityRequirement { + check: |f| f.has_team("away"), + error_field: "teams/away".to_string(), + }, + // teams.away.id required + EntityRequirement { + check: |f| { + if f.has_team("away") { + f.has_team_attr("away", TrackedEntityAttribute::Id) + } else { + true + } + }, + error_field: "teams/away/id".to_string(), + }, + // teams.away.players required (at least one) + EntityRequirement { + check: |f| { + if f.has_team("away") { + f.has_any_player("away") + } else { + true + } + }, + error_field: "teams/away/players".to_string(), + }, + // no other teams allowed + EntityRequirement { + check: |f| { + f.teams() + .iter() + .all(|t| t == "home" || t == "away" || t == "none") + }, + error_field: "teams/".to_string(), + }, + ], + value_requirements: vec![ValueRequirement { + column: "period".to_string(), + allowed_values: vec![ + "first_half".to_string(), + "second_half".to_string(), + "first_half_extratime".to_string(), + "second_half_extratime".to_string(), + "shootout".to_string(), + ], + }], + core_columns: vec![ + "match/id", + "competition/id", + "season/id", + "timestamp", + "frame_id", + "period", + "period_id", + "player/id", + "team/id", + "entity_key", + "ball_status", + "ball_poss_team_id", + "ball_poss_status", + ] + .into_iter() + .map(String::from) + .collect(), + } +} + +/// CDF v0.2.3 skeletal tracking data. +/// +/// Based on +/// +/// Required top-level: frame_id, timestamp, period, match (with id), teams (home & away). +/// Ball is optional. Each team requires: id, players (each with id, landmarks). +/// Each landmark requires: index, name, x, y, z, children, is_visible. +pub fn cdf_skeletal_v0_2_3() -> SchemaDefinition { + SchemaDefinition { + id: SchemaId::new("cdf", "skeletal", "0.2.3"), + description: "CDF v0.2.3 skeletal tracking data".to_string(), + required_frame_attributes: vec![ + FrameAttribute::FrameId, + FrameAttribute::Timestamp, + FrameAttribute::Period, + FrameAttribute::MatchId, + ], + required_ball_attributes: vec![], + entity_requirements: vec![ + // teams.home required + EntityRequirement { + check: |f| f.has_team("home"), + error_field: "teams/home".to_string(), + }, + // teams.home.id required + EntityRequirement { + check: |f| { + if f.has_team("home") { + f.has_team_attr("home", TrackedEntityAttribute::Id) + } else { + true + } + }, + error_field: "teams/home/id".to_string(), + }, + // teams.home.players required (at least one) + EntityRequirement { + check: |f| { + if f.has_team("home") { + f.has_any_player("home") + } else { + true + } + }, + error_field: "teams/home/players".to_string(), + }, + // teams.away required + EntityRequirement { + check: |f| f.has_team("away"), + error_field: "teams/away".to_string(), + }, + // teams.away.id required + EntityRequirement { + check: |f| { + if f.has_team("away") { + f.has_team_attr("away", TrackedEntityAttribute::Id) + } else { + true + } + }, + error_field: "teams/away/id".to_string(), + }, + // teams.away.players required (at least one) + EntityRequirement { + check: |f| { + if f.has_team("away") { + f.has_any_player("away") + } else { + true + } + }, + error_field: "teams/away/players".to_string(), + }, + // At least one player must have landmarks + EntityRequirement { + check: |f| f.has_any_landmark(), + error_field: "teams/*/players/*/landmarks".to_string(), + }, + // no other teams allowed + EntityRequirement { + check: |f| { + f.teams() + .iter() + .all(|t| t == "home" || t == "away" || t == "none") + }, + error_field: "teams/".to_string(), + }, + ], + value_requirements: vec![ValueRequirement { + column: "period".to_string(), + allowed_values: vec![ + "first_half".to_string(), + "second_half".to_string(), + "first_half_extratime".to_string(), + "second_half_extratime".to_string(), + "shootout".to_string(), + ], + }], + core_columns: vec![ + "match/id", + "competition/id", + "season/id", + "timestamp", + "frame_id", + "period", + "period_id", + "player/id", + "team/id", + "entity_key", + ] + .into_iter() + .map(String::from) + .collect(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_registry_contains_all_schemas() { + let schemas = list_schemas(); + assert_eq!(schemas.len(), 2); + } + + #[test] + fn test_lookup_cdf_tracking() { + let id = SchemaId::new("cdf", "tracking", "0.2.3"); + let schema = get_schema(&id); + assert!(schema.is_some()); + assert_eq!(schema.unwrap().id, id); + } + + #[test] + fn test_lookup_cdf_skeletal() { + let id = SchemaId::new("cdf", "skeletal", "0.2.3"); + let schema = get_schema(&id); + assert!(schema.is_some()); + assert_eq!(schema.unwrap().id, id); + } + + #[test] + fn test_lookup_unknown_schema() { + let id = SchemaId::new("basketball", "tracking", "1.0.0"); + assert!(get_schema(&id).is_none()); + } + + #[test] + fn test_default_schema_id() { + let id = default_schema_id(); + assert_eq!(id, SchemaId::new("cdf", "tracking", "0.2.3")); + } +} diff --git a/crates/tdc-parsers/src/hawkeye.rs b/crates/tdc-parsers/src/hawkeye.rs index 53d6b38..562ebf7 100644 --- a/crates/tdc-parsers/src/hawkeye.rs +++ b/crates/tdc-parsers/src/hawkeye.rs @@ -419,7 +419,7 @@ pub fn load_hawkeye( } } - let final_builder = builders + let mut final_builder = builders .into_iter() .reduce(|mut a, b| { a.merge(b); @@ -427,6 +427,14 @@ pub fn load_hawkeye( }) .ok_or("No data loaded")?; + final_builder.get_or_create_column_id("match/id", arrow_schema::DataType::Utf8); + final_builder.get_or_create_column_id( + "timestamp", + arrow_schema::DataType::Timestamp(arrow_schema::TimeUnit::Microsecond, None), + ); + final_builder.get_or_create_column_id("teams/home/id", arrow_schema::DataType::Utf8); + final_builder.get_or_create_column_id("teams/away/id", arrow_schema::DataType::Utf8); + let mut container = final_builder.build().map_err(|e| e.to_string())?; let mut x_range = None; @@ -454,6 +462,9 @@ pub fn load_hawkeye( Some(LengthUnit::Meters), ); container = container.with_coordinate_system(cs); + container = + container.with_schema_id(tdc_core::schema::SchemaId::new("cdf", "skeletal", "0.2.3")); + container.validate().map_err(|e| e.to_string())?; Ok(container) } diff --git a/crates/tdc-parsers/src/sportec.rs b/crates/tdc-parsers/src/sportec.rs index dd63aec..820f3aa 100644 --- a/crates/tdc-parsers/src/sportec.rs +++ b/crates/tdc-parsers/src/sportec.rs @@ -185,8 +185,34 @@ pub fn load_sportec(path: &str, meta_path: Option<&str>) -> Result = global_frames.keys().cloned().collect(); - sorted_n.sort_unstable(); + let mut raw_sorted_n: Vec = global_frames.keys().cloned().collect(); + raw_sorted_n.sort_unstable(); + + let mut sorted_n = Vec::new(); + let mut valid_periods = Vec::new(); + let mut current_period = "other"; + + for &n in &raw_sorted_n { + let (_, p_id) = global_frames.get(&n).unwrap(); + if let Some(p) = p_id { + current_period = match p { + 1 => "first_half", + 2 => "second_half", + 3 => "first_half_extratime", + 4 => "second_half_extratime", + 5 => "shootout", + _ => "other", + }; + } + if current_period != "other" { + sorted_n.push(n); + valid_periods.push(current_period.to_string()); + } + } + + if sorted_n.is_empty() { + return Err("No valid frames found in Sportec data after period filtering".to_string()); + } let n_to_row: AHashMap = sorted_n.iter().enumerate().map(|(i, &n)| (n, i)).collect(); @@ -427,22 +453,10 @@ pub fn load_sportec(path: &str, meta_path: Option<&str>) -> Result "first_half", - 2 => "second_half", - 3 => "first_half_extratime", - 4 => "second_half_extratime", - 5 => "shootout", - _ => "other", - } - .to_string(); - } - final_builder.insert_string_at_idx(i, c_p, &last_p); + final_builder.insert_string_at_idx(i, c_p, &valid_periods[i]); final_builder.insert_int_at_idx(i, c_fid, n); if let Some(ts) = ts_str @@ -465,6 +479,9 @@ pub fn load_sportec(path: &str, meta_path: Option<&str>) -> Result Result { let num_cpus = rayon::current_num_threads(); let chunk_size = lines.len().div_ceil(num_cpus); - let final_builder = lines + let mut final_builder = lines .par_chunks(chunk_size) .map(|chunk| -> Result { let mut builder = TrackingDataBuilder::new(); @@ -146,6 +146,10 @@ pub fn load_statsperform(path: &str) -> Result { }, )?; + final_builder.get_or_create_column_id("match/id", DataType::Utf8); + final_builder.get_or_create_column_id("teams/home/id", DataType::Utf8); + final_builder.get_or_create_column_id("teams/away/id", DataType::Utf8); + let mut container = final_builder.build().map_err(|e| e.to_string())?; let cs = CoordinateSystem::new( @@ -157,6 +161,9 @@ pub fn load_statsperform(path: &str) -> Result { Some(LengthUnit::Meters), ); container = container.with_coordinate_system(cs); + container = + container.with_schema_id(tdc_core::schema::SchemaId::new("cdf", "tracking", "0.2.3")); + container.validate().map_err(|e| e.to_string())?; Ok(container) } diff --git a/crates/tdc-parsers/tests/test_sportec.rs b/crates/tdc-parsers/tests/test_sportec.rs index e024257..b849840 100644 --- a/crates/tdc-parsers/tests/test_sportec.rs +++ b/crates/tdc-parsers/tests/test_sportec.rs @@ -23,6 +23,9 @@ fn test_sportec_parser() { + + + "#; diff --git a/crates/tdc-python/src/container.rs b/crates/tdc-python/src/container.rs index a3085f0..53335b1 100644 --- a/crates/tdc-python/src/container.rs +++ b/crates/tdc-python/src/container.rs @@ -1,6 +1,6 @@ use crate::coordinate_system::{PyCoordinateSystem, PyOrientationMode}; use crate::error::IntoPyResult; -use crate::schema::{PyDataField, PyFields}; +use crate::schema::{PyDataField, PyFields, PySchemaId}; use crate::selector::{PyColumnSelector, PyExpr, PySelection}; use tdc_core::Select; use tdc_core::config::{CONTEXT, RUNTIME}; @@ -130,6 +130,7 @@ impl PyTrackingDataContainer { }, coordinate_system: coordinate_system.map(|cs| cs.inner), orientation, + schema_id: None, }, }) } @@ -173,6 +174,7 @@ impl PyTrackingDataContainer { schema: Fields { fields }, coordinate_system: coordinate_system.map(|cs| cs.inner), orientation, + schema_id: None, }, }) } @@ -333,6 +335,28 @@ impl PyTrackingDataContainer { self.inner.validate().into_pyresult() } + #[getter] + fn schema_id(&self) -> Option { + self.inner + .schema_id + .clone() + .map(|id| PySchemaId { inner: id }) + } + + fn with_schema_id(&self, schema_id: &PySchemaId) -> Self { + PyTrackingDataContainer { + inner: self.inner.clone().with_schema_id(schema_id.inner.clone()), + } + } + + fn validate_with_schema(&self, schema_id: &PySchemaId) -> PyResult<()> { + use crate::error::IntoPyResult; + self.inner + .schema + .validate_with_schema(&schema_id.inner) + .into_pyresult() + } + fn save_parquet(&self, path: &str) -> PyResult<()> { self.inner.save_parquet(path).into_pyresult() } @@ -832,7 +856,7 @@ pub fn scan_parquet(_py: Python<'_>, path: String) -> PyResult( - LazyTrackingDataContainer::new(df, schema, None, None), + LazyTrackingDataContainer::new(df, schema, None, None, None), ) }) .map_err(|e| PyValueError::new_err(e.to_string()))?; diff --git a/crates/tdc-python/src/schema.rs b/crates/tdc-python/src/schema.rs index 0d2d07f..58d5bcb 100644 --- a/crates/tdc-python/src/schema.rs +++ b/crates/tdc-python/src/schema.rs @@ -184,6 +184,12 @@ impl PyFields { index: 0, } } + fn validate_with_schema(&self, schema_id: &PySchemaId) -> PyResult<()> { + use crate::error::IntoPyResult; + self.inner + .validate_with_schema(&schema_id.inner) + .into_pyresult() + } } #[pyclass] @@ -210,10 +216,82 @@ impl FieldIterator { } } +#[pyclass(name = "_SchemaId")] +#[derive(Clone)] +pub struct PySchemaId { + pub inner: tdc_core::schema::SchemaId, +} + +#[pymethods] +impl PySchemaId { + #[new] + fn new(sport: String, data_kind: String, version: String) -> Self { + Self { + inner: tdc_core::schema::SchemaId::new(sport, data_kind, version), + } + } + + #[staticmethod] + fn from_string(s: &str) -> PyResult { + tdc_core::schema::SchemaId::parse(s) + .map(|inner| Self { inner }) + .ok_or_else(|| { + pyo3::exceptions::PyValueError::new_err(format!( + "Invalid schema ID: '{}'. Expected format: 'sport/data_kind/version'", + s + )) + }) + } + + #[getter] + fn sport(&self) -> &str { + &self.inner.sport + } + + #[getter] + fn data_kind(&self) -> &str { + &self.inner.data_kind + } + + #[getter] + fn version(&self) -> &str { + &self.inner.version + } + + fn __str__(&self) -> String { + self.inner.to_string() + } + + fn __repr__(&self) -> String { + format!("SchemaId({})", self.inner) + } + + fn __eq__(&self, other: &Self) -> bool { + self.inner == other.inner + } + + fn __hash__(&self) -> u64 { + use std::hash::{Hash, Hasher}; + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + self.inner.hash(&mut hasher); + hasher.finish() + } +} + +#[pyfunction] +pub fn list_schemas() -> Vec { + tdc_core::schema::list_schemas() + .iter() + .map(|s| s.to_string()) + .collect() +} + pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(list_schemas, m)?)?; Ok(()) } diff --git a/examples/01-basics.ipynb b/examples/01-basics.ipynb index 7ecc21c..2b9781b 100644 --- a/examples/01-basics.ipynb +++ b/examples/01-basics.ipynb @@ -93,6 +93,7 @@ " \"match/id\": [\"match_123\", \"match_123\", \"match_123\"],\n", " \"ball/x\": [50.0, 51.0, 52.0],\n", " \"ball/y\": [12.0, 13.0, 14.0],\n", + " \"ball/z\": [0.0, 0.0, 0.0],\n", " \"teams/home/id\": [\"team_A\", \"team_A\", \"team_A\"],\n", " \"teams/home/players/p1/x\": [23.5, 23.6, 23.7],\n", " \"teams/home/players/p1/y\": [24.2, 24.3, 24.4],\n", @@ -101,7 +102,7 @@ " \"teams/away/players/p2/y\": [34.9, 35.0, 35.1],\n", "}\n", "\n", - "container = tdc.TrackingDataContainer(data, validate=False)\n", + "container = tdc.TrackingDataContainer(data, validate=True)\n", "print(f\"Container created with {container.num_rows()} frames.\")" ] }, @@ -200,6 +201,26 @@ "TDC uses the **Common Data Format (CDF)** standard for naming columns. This standard defines a hierarchical path structure (e.g., `teams/{team_id}/players/{player_id}/x`) that allows for standardized selection logic regardless of the data provider." ] }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['cdf/skeletal/0.2.3', 'cdf/tracking/0.2.3']" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "tdc.list_schemas()" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -211,7 +232,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -219,10 +240,10 @@ "output_type": "stream", "text": [ "Ball columns (using selector):\n", - "['ball/x', 'ball/y']\n", + "['ball/x', 'ball/y', 'ball/z']\n", "\n", "Ball columns (using wildcard 'ball/**'):\n", - "['ball/x', 'ball/y']\n", + "['ball/x', 'ball/y', 'ball/z']\n", "\n", "Player X columns:\n", "['teams/home/players/p1/x', 'teams/away/players/p2/x']\n" @@ -257,14 +278,14 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Schema validation failed: Missing ball/z\n" + "Schema is valid CDF!\n" ] } ], @@ -277,6 +298,47 @@ " print(f\"Schema validation failed: {e}\")" ] }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Schema validation failed: schema error: Invalid value for column 'period': wrong_half\n" + ] + } + ], + "source": [ + "try: \n", + " base_time = datetime(2023, 5, 27, 13, 30, 0, tzinfo=timezone.utc)\n", + " data = {\n", + " \"frame_id\": [1000, 1001, 1002],\n", + " \"timestamp\": [\n", + " base_time,\n", + " base_time + timedelta(milliseconds=40),\n", + " base_time + timedelta(milliseconds=80),\n", + " ],\n", + " \"period\": [\"first_half\", \"first_half\", \"wrong_half\"], # <-- invalid value\n", + " \"match/id\": [\"match_123\", \"match_123\", \"match_123\"],\n", + " \"ball/x\": [50.0, 51.0, 52.0],\n", + " \"ball/y\": [12.0, 13.0, 14.0],\n", + " \"ball/z\": [0.0, 0.0, 0.0],\n", + " \"teams/home/id\": [\"team_A\", \"team_A\", \"team_A\"],\n", + " \"teams/home/players/p1/x\": [23.5, 23.6, 23.7],\n", + " \"teams/home/players/p1/y\": [24.2, 24.3, 24.4],\n", + " \"teams/away/id\": [\"team_B\", \"team_B\", \"team_B\"],\n", + " \"teams/away/players/p2/x\": [33.1, 33.2, 33.3],\n", + " \"teams/away/players/p2/y\": [34.9, 35.0, 35.1],\n", + " }\n", + " tdc.TrackingDataContainer(data, validate=True)\n", + " print(\"Schema is valid CDF!\")\n", + "except tdc.exceptions.SchemaError as e:\n", + " print(f\"Schema validation failed: {e}\")" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -286,7 +348,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -315,7 +377,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -328,10 +390,10 @@ "Column: match/id | Entity: / | Attribute: match\n", "Column: ball/x | Entity: Ball | Attribute: x\n", "Column: ball/y | Entity: Ball | Attribute: y\n", + "Column: ball/z | Entity: Ball | Attribute: z\n", "Column: teams/home/id | Entity: Team | Attribute: id\n", "Column: teams/home/players/p1/x | Entity: Player | Attribute: x\n", - "Column: teams/home/players/p1/y | Entity: Player | Attribute: y\n", - "Column: teams/away/id | Entity: Team | Attribute: id\n" + "Column: teams/home/players/p1/y | Entity: Player | Attribute: y\n" ] } ], diff --git a/examples/02-parsing.ipynb b/examples/02-parsing.ipynb index c6e0e7e..7230d85 100644 --- a/examples/02-parsing.ipynb +++ b/examples/02-parsing.ipynb @@ -35,7 +35,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Ingested 10000 frames in 0.0768 seconds.\n" + "Ingested 10000 frames in 0.0794 seconds.\n" ] } ], @@ -158,9 +158,9 @@ "output_type": "stream", "text": [ "--- Loading benchmark ---\n", - "TDC (Rust): 1.41s\n", - "Fast-Forward: 1.69s\n", - "Kloppy: 20.04s\n" + "TDC (Rust): 1.67s\n", + "Fast-Forward: 2.06s\n", + "Kloppy: 22.91s\n" ] } ], @@ -198,7 +198,7 @@ "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA9sAAAGGCAYAAABrBpLcAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAXC5JREFUeJzt3Qd4FOXaxvE3hRKBhN6RJiKiYkfsisfeK9grKjbs7Shy7L0XbGBXVOweC1ZUVEAERFCaCAhSlN6T+a77Pc5+m2STTcIk+2z4/65rCdk6O/fuZp9533kmIwiCwAEAAAAAgMhkRndXAAAAAACAYhsAAAAAgErAyDYAAAAAABGj2AYAAAAAIGIU2wAAAAAARIxiGwAAAACAiFFsAwAAAAAQMYptAAAAAAAiRrENAAAAAEDEKLYBYAPw+eefu4yMDPfaa6+5dHDDDTf45V2wYEGqFwXwfvvtN/+aHDx4cLHXaXW3bNkyd+aZZ7rmzZv759uvX79ULxIApAWKbQBYT/ryrS+g8aemTZu6vfbay/33v/9l/RryzjvvuD322MPns9FGG7kOHTq4Y4891n3wwQcpW6YXX3zR3XfffVX+uEEQuN122801adLELVy4sNjl55xzjqtRo4b78ccfCxWbOt10000J7/OEE07wl9etW7fQ+XvuuafbYostqv1Goqp06qmnFvrMyc3Ndd26dXN33323W716daSPdcstt/jPuXPPPdc999xz7qSTTor0/gGguqLYBoCI/Oc///FfRJ999ll3xRVXuPnz57sDDzzQvfvuu6xjA+666y536KGH+sLk6quvdvfee6876qij3OTJk93LL7+8wRXbWg8DBw50ixcvdpdddlmhy0aMGOEef/xxd9FFF7mtt9660GW1a9d2L730UrH7W758uXvrrbf85RuKf//7327lypUpe/xatWr5zxydVBA3bNjQZ3nKKadE+jiffvqp22mnnVz//v3diSee6LbbbrtI7x8AqqvsVC8AAFQXBxxwgNt+++1jv59xxhmuWbNmvjA5+OCDU7ps6UIFW506dSK/33Xr1rkbb7zR/etf/3IfffRRscvnzZvnqstzLY/NN9/cXX755b5Q00ipRv3Xrl3r+vTp49q0aeMGDBhQ7DbagDR06FA3duxYP5IaUqG9Zs0at//++/vibEOQnZ3tT6l8fBW/ob59+7ru3bu7V155xd1zzz2uZcuWFb7vgoICn6c2nuj9oddKlO9H3X/NmjUju08AsIiRbQCoJPXr13c5OTnFvozrS6ZGMrt27eq/yKogP/vss93ff/9d6Hrt2rXzRfpXX33ldtxxR39dTXvWyHlRixYtchdffLG/jUa7Wrdu7U4++eRi+zzrsW+++WZ/ue6vZ8+ebsqUKQmn/I4bN84XX5puvckmm8Sm8n7xxRf+C72eW+fOnd2wYcMK3X7GjBn+S78u03UaNWrkjjnmGD8NOdH0e92frq+p3Vqukuh+tRxatj///NOft2LFCjdp0qSk+3br8iVLlrhddtkl4eV67KJTl1WwXHPNNX4/VRXFGhWfOXNmsdu++uqrfqRPz7Vx48a++Jk9e3ah66iQ1dTqqVOn+mK1Xr16fsq11vV7773nn1s4HVgZhjQdWKOJet7KVQWwZk0kmib8/PPP+9eJ8mrQoIHbfffdE25YKOq6665zHTt29K9BFVeahvzTTz+5hx56KOHGgB49erj27dv7Efl4L7zwgi+0Nbpa1aZNm+ZfY3psPX+Nwmq9xtNzu/76631WeXl5/rlpGv1nn32W8P2kzHQ9vY81Uqzzikq0z7Z+P//8892bb77pX6vKTe/1RLsq6LWmDXR6LyoDzTRYn/3AMzMz/WtKwvdbWV9D4XIrRy2vrqtl1vnTp0/36zN8jYb3rSI83Kio56CNL88880yh+w13P9DMEn3u6Xnqvn/++efYc/3111/9+0brW7s16DWp3Rz0fjvssMP8FHm9D/XarEim8cugGRvhMuywww5u5MiRxdajPlO0e4mWJfycu/baawtdR+/x008/3T/3MOOnn366QrkBqL4Y2QaAiGg6roo6fUnUl9AHH3zQNxaKH3kSFTUqNE877TR34YUX+i+yKmzGjBnjvv76a7+fbEiF8NFHH+2/0OoLv77MqQjQl0t9uRM9hr5gTpw40X/523bbbf1yvP32227WrFm+AAzddttt/gu5pppqee+44w5f9H333XeFllGFvwr9Xr16+SLm0Ucf9f/XF3E1R9L+vMcff7y78847/fLpS7EKSNGX12+++cZfX8Wzvujq9ioC9AVbxVA8Fdr6UqsvzRrtTURF6t577+2LqY8//jj2nL7//nu/b7yKCX1xL4mKaX1p1j7bF1xwQZkKQm2U0Bf0K6+80uepQmGfffbx+zDrviTMUV/ab731Vr8R4P777/c5Kk8VavGjefvtt5/bdddd/Zd+rQcVEMpBOWlau4T7O2vDiAp8bWzRSHOXLl3c+PHj/fVUnKiYC2kEWs9/55139rszaMRQmWqEed999y31eapIeuSRR/yyKQsV0UcccYQ75JBDSrxN7969fXGv11PYyE6FvaYzV/X+71rnet7a8KL3kzbuqODTutMGIj0X0caWJ5980i/7WWed5ZYuXeqeeuop/7z1Ogqny+v9qwJP612vc633N954o1xTs3Vbjf5rfep98cADD/hdFn7//Xe/fKLXhzZOtGjRwueXn5/vs9N7YX3ovSJ6nPK8hkSvlyFDhviiW+8xLZsy1YY8vZcvvfRSfz0to6bP6z2tzyhdXxtgtOFJn0/aMKFdEOINGjTIrVq1yi+HitP49+Bxxx3nl02vJxX16gmgy7XxQe/722+/3X/26HNL7zVtSCpPpiG9tnUdfQbrdavPvyOPPNJvrAk/d7WRUZ+n+l3Lqo1fWqf67NBnQvia0wadcAOF1of6c+hzWstEAzkAMQEAYL0MGjQo0Mdp0VOtWrWCwYMHF7ru8OHD/WUvvPBCofM/+OCDYue3bdvWn/fll1/Gzps3b56/30svvTR23vXXX++vN3To0GLLVlBQ4H9+9tln/jpdunQJVq9eHbv8/vvv9+ePHz8+dt4ee+zhz3vxxRdj502aNMmfl5mZGXz77bex8z/88EN/vtZBaMWKFcWWY8SIEf56zz77bLH1tuuuuwbr1q0rdP3+/fv7y+bPnx9MnDgxaNmyZbDDDjsEf/31V6Hrhc9L108mXE916tQJDjjggODmm28ORo8eXex64X22atUqWLJkSez8IUOG+PO1zmTNmjVB06ZNgy222CJYuXJl7Hrvvvuuv54eL3TKKaf486666qpij3fQQQf5rIt67rnn/PrWaybeY4895u/r66+/9r9PnjzZX++II44I8vPzE+ZfFr179/b3W69evWDmzJnFLp8+fbq//M477wx++ukn//9w2R5++OGgbt26wfLly/1z1TqOp9dU165dg/IKs3j11VdLvE6/fv0KLYssXbo0aN++fdCuXbvYOtFrLP61L3///XfQrFmz4PTTT4+d9+abb/r7u+OOO2Ln6ba77bZbsdd6+DqNp99r1qwZTJkyJXbe2LFj/fkPPvhg7LxDDjkk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AAAAZbB06VKXl5fnqhLFdppp2LCh//n7779X+YsFJW8ta9OmjZs5c6bLzc1lNaUYedhDJvaQiT1kYgt52EMm9qRLJkEQ+EK7ZcuWVf7YFNtpenxtFdqWX9QbIuVBJnaQhz1kYg+Z2EMmtpCHPWRiTzpkkpeiQUoapAEAAAAAEDGKbQAAAAAAIkaxnWZq1arl+vfv73/CBjKxhTzsIRN7yMQeMrGFPOwhE3vIJLmMIBU90AEAAAAAqMYY2QYAAAAAIGIU2wAAAAAARIxiGwAAAACAiFFsG/Twww+7du3audq1a7vu3bu777//vtTrv/rqq26zzTbz199yyy3d+++/X2XLWt3deuutbocddnD16tVzTZs2dYcffrj75ZdfSr3N4MGDXUZGRqGTssH6u+GGG4qtW732S8P7o3Lps6poJjqdd955Ca/P+yN6X375pTvkkENcy5Yt/bp/8803C12u1izXX3+9a9GihcvJyXH77LOPmzx5cuR/i1C2TNauXeuuvPJK//e6Tp06/jonn3yy++OPPyL//EPZMpFTTz212Prdf//9k65C3ieVk0eivys63XnnnSXeJ++Ryv/Ou2rVKv/3vVGjRq5u3bruqKOOcn/++Wep9xtU8G9QdUGxbcwrr7ziLrnkEt9x/IcffnDdunVz++23n5s3b17C63/zzTeud+/e7owzznBjxozxbwydfvrppypf9uroiy++8B8q3377rfv444/9l6R9993XLV++vNTb5ebmujlz5sROM2bMqLJlru66du1aaN1+9dVXJV6X90flGzlyZKE89D6RY445psTb8P6Ilj6P9LdCX/oTueOOO9wDDzzgHnvsMffdd9/5Ak9/V/SlKaq/RSh7JitWrPDr9LrrrvM/hw4d6r/QHnrooZF+/qHsmYRUXMev35deeqnU1cj7pPLyiM9Bp6efftoX2yruSsN7pHK/81588cXunXfe8QMZur42Eh555JGl3u8dFfgbVK2oGzns2HHHHYPzzjsv9nt+fn7QsmXL4NZbb014/WOPPTY46KCDCp3XvXv34Oyzz670Zd0QzZs3T937gy+++KLE6wwaNCjIy8ur0uXaUPTv3z/o1q1bma/P+6PqXXTRRUHHjh2DgoKChJfz/qhc+nx64403Yr8rh+bNmwd33nln7LxFixYFtWrVCl566aXI/hah7Jkk8v333/vr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wQdu2LBhsfPy8vJcTk5Oylb3Aw884E477TSXmbnhbOOvWbOmO/744/1z32233VK9OACAFNhw/uoBAFAFvvjiC7fjjju6WrVquRYtWrirrrrKrVu3Lna5CuFdd93V1a9f3zVq1MgdfPDBburUqYXu4/vvv3fbbLONq127ttt+++3dmDFjSny8rKws17x589ipbt26Ljs7u9B5KrSLTiNv166du+mmm9zJJ5/sb9O2bVs/Aj1//nx32GGH+fO22morN2rUqEKP99VXX/niUfepQv/CCy90y5cvL3H5dH+ffvppoSnVQRD4Kdkbb7yxX08tW7b09xNavXq1u+yyy1yrVq1cnTp1XPfu3d3nn39e6H6//vpr/5w22mgj16BBA7fffvu5v//+O3Z73V/Tpk39OtT6HjlyZOy2ui+N+H/yySd+/eo+dt55Z/fLL78UeozbbrvNNWvWzNWrV8+dccYZbtWqVYUu1/0oay2j8txll13cjBkzYpfrOWudrly5ssT1AwCovii2AQCIyOzZs92BBx7odthhBzd27Fj36KOPuqeeesoXtSEVppdccokvYlXsabT3iCOOcAUFBf7yZcuW+QJ88803d6NHj/ZFqQrPynDvvff6AlHF/EEHHeROOukkX3yfeOKJfgp0x44d/e8qjkUbBfbff3931FFHuXHjxrlXXnnFF9/nn39+iY+hy1XMdunSJXbe66+/7h974MCBbvLkye7NN990W265Zexy3d+IESPcyy+/7B/nmGOO8Y+r64r2Xe/Zs6dfR7qeHkOFbX5+vr/8iiuu8I/xzDPP+OexySab+GL8r7/+KrRs1157rbv77rt9FtpAoanfoSFDhvh1f8stt/jLteHkkUceiV2uDSjanWCPPfbwy6jl6NOnjy/iQyrkdb3vvvtuPZMCAKSlAAAAlNkpp5wSHHbYYQkvu+aaa4LOnTsHBQUFsfMefvjhoG7dukF+fn7C28yfP1+VbDB+/Hj/+8CBA4NGjRoFK1eujF3n0Ucf9dcZM2ZM0uXr379/0K1bt2Ln77HHHsFFF10U+71t27bBiSeeGPt9zpw5/jGuu+662HkjRozw5+kyOeOMM4I+ffoUut/hw4cHmZmZhZY33r333ht06NCh0Hl33313sOmmmwZr1qwpdv0ZM2YEWVlZwezZswud37Nnz+Dqq6/2/+/du3ewyy67JHy8ZcuWBTVq1AheeOGF2Hl6nJYtWwZ33HGH//2zzz7zz2vYsGGx67z33nv+vPB59OjRI+jbt2+h++7evXts3S5cuNBf//PPPw9K06BBg2Dw4MGlXgcAUD0xsg0AQEQmTpzoevToUWh0UyPHGq2eNWuW/12js71793YdOnRwubm5fjq3/P7777H70PRtTX8O6T4rgx4npOnSEj/CHJ43b948/1Oj9Wq2pinm4UkjxhqVnz59esLH0BTq+OciGqnW+VoHZ511lnvjjTdiU+3Hjx/vR6g33XTTQo+j6fnhdPtwZDsRXWft2rV+vYdq1Kjhp3tr3Zb0/DVyHf9cdV1NX48Xn0PDhg19szw9f42q33///YX2nQ9puv2KFSsSLisAoHqjQRoAAFVIhZn2j37iiSf8vsoqVLfYYgu3Zs2aKs9BRWgo3ECQ6Lz4Ke5nn312of2rQ9r/OpHGjRvH9qUOaV9v7R+tJm4ff/yx69u3r7vzzjt9Qa3H0H7omkKvn/FUdEtUzd5Ke65lMWjQIL8utB++ptT/+9//9s9np512il1HU9ebNGkSyfICANILI9sAAERE+yVr391wH+ewkZcabLVu3dotXLjQF5kqyjQyq+sXLUR1nvYBjm/G9e2335rIaNttt3U///yz3we66EndtxNRo7e5c+cWe54qmLXhQd261WhM602j2rq+RrY1wlz0MdTsLRyR1v7uiWg/cy2L1ntII91qkKZ9vMtKORTd1zpRDlreq6++2n3zzTd+o0n8scQ1yq4cdR0AwIaHYhsAgHJavHixn8ocf5o5c6YfodXPCy64wE2aNMm99dZbrn///r4hmhqhqWu2OpA//vjjbsqUKb5Lty6Lp8NFaZRV06tV2L7//vv+cF4WXHnllb6oVAMzPWdNiddzLK1BmgpNjW7HF7+aiq7GcT/99JObNm2ae/75533xrRF/TR8/4YQTfGM2HRtc09PVnf3WW2917733nr+9ilsVz1rf2jChda1mdAsWLPCdwc8991x3+eWX+xFnrUOtS03lVkfxsrrooovc008/7Uevf/31V5/jhAkTYpdrubQc2kigDuQfffSRXx/xjeB0jG1NldcGAADAhodp5AAAlJNGYouOVqqQe/LJJ31xrEKvW7dufr9ena+RbFHBrQ7bmnqsUdDOnTv7kV0dwip+qvQ777zjzjnnHP8YGo29/fbbfQfwVNOIsqZ6q4u3Dv+lEXwVkscdd1yJt9FUcB1j+4UXXvBd1kWHydJhtbShQaPY2k9cz1kbIkQFrjq4X3rppb7Du4p1Tc0Ob6+CXMXtNddc4/fFVqGu/au1L7zovjUdXN3Vly5d6ruCf/jhh35jR1npOWlkWp3NNTqt9a8iXvcj6rCuIl8dzzVjQft8n3feeX6afeill17yhT4AYMOUoS5pqV4IAABQfWkaedeuXf1huDR6vSHQKPjee+/tR8Xz8vJSvTgAgBRgGjkAAKhU2tda08bDjusbAnUmf/bZZym0AWADxsg2AAAAAAARY2QbAAAAAICIUWwDAAAAABAxim0AAAAAACJGsQ0AAAAAQMQotgEAAAAAiBjFNgAAAAAAEaPYBgAAAAAgYhTbAAAAAABEjGIbAAAAAICIUWwDAAAAABCx/wNYKolsZy2XPgAAAABJRU5ErkJggg==", + "image/png": "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zdh6KLHGjh3runTp4urUqePj9f333892u9qd3HHHHa527douLS3NnXjiiW7OnDkJW1+gMPF7ySWX5PpMPvnkk9mIKBHuu+8+16ZNG1epUiVXs2ZNd+aZZ7pff/012322bt3qrrnmGletWjVXsWJFd84557g//vgjYesM5IVk24gRI0a4G2+80Xci//nnn12rVq1c586d3cqVKxO9akCBHHzwwe7333+PXr799lu2HEqszZs3+89ZHeSMZ/Dgwe6JJ55wQ4YMcT/++KOrUKGC/0zWF0CgpMevKLmO/Ux+4403inUdgbx88803PpH+4Ycf3Oeff+527NjhTjrpJB/XgRtuuMGNGjXKvfXWW/7+y5cvd2effTYbFSUO3ciNUCVbR/meeuop/3dWVparV6+eu/baa13//v0TvXrALivbqqxMmTKFLQVzVPV77733fHUlqGqrYti3b1930003+evWr1/v9t13Xzd06FB34YUXJniNgbzjN6hsr1u3LlfFGyiJVq1a5SvcSqqPOeYY/3lbo0YN9/rrr7tzzz3X32f27NmuefPmbvz48e6oo45K9CoDUVS2Ddi+fbubNGmSH6YYSE5O9n/rQwWwQENslaA0atTIdevWzS1evDjRqwQUyYIFC9yKFSuyfSZnZGT4g6J8JsOKr7/+2icwTZs2dVdddZVbs2ZNolcJiEvJtVStWtX/1HdiVbtjP4M1NW3//ffnMxglDsm2AatXr3aZmZm+ahJLf+sLH1DSKQlRxW/06NHu2Wef9clKx44d3caNGxO9akChBZ+7fCbDKg0hHzZsmBszZox74IEHfMXwlFNO8d81gJJEIzn79Onj2rdv7w455JDoZ3DZsmVd5cqVs92X78UoiVITvQIASj99iQu0bNnSJ9/169d3I0eOdL169UrougHA3iZ2qkOLFi3853Ljxo19tbtTp04JXTcgluZuT58+nT4vMIvKtgHVq1d3KSkpubos6u9atWolbL2AotLR6AMPPNDNnTuXjQhzgs9dPpNRWmh6j75r8JmMkqR3797uo48+cl999ZWrW7duts9gTbFU34FYfC9GSUSybYCGyhx++OF+uFfssBr93a5du4SuG1AUmzZtcvPmzfOnTQKsadiwof+yF/uZvGHDBt+VnM9kWLR06VI/Z5vPZJQEakKpRFuN/b788kv/mRtL34nLlCmT7TNYpwZTLxg+g1HSMIzcCJ32q0ePHu6II45wRx55pHvsscf8KRB69uyZ6FUDdkkdm3XOVw0d1+k5dAo7jdbo2rUrWw8l9oBQbJVPfQbUTV8NetSER3MI7777btekSRP/RfD222/3DQBjOz4DJTF+dbnzzjv9eYl10EgHPvv16+cOOOAAf/o6oCQMHVen8Q8++MCfazvok6FGlGlpaf6npqDpu7HiOT093Z+dR4k2nchR0nDqL0N02q8HH3zQf+i0bt3an+NVc18BC/MDx44d6ysnOl1Hhw4d3D333OPnCAIlkeauHn/88bmu10FPNftT5UUHjZ5//nk/lFEx/cwzz/jpEUBJjl81qdRBocmTJ/vY1UEincP4rrvuytX0D0jU6erieeWVV/xp62Tr1q3+9Is6P/y2bdv8gSJ9BjO9EiUNyTYAAAAAACFjzjYAAAAAACEj2QYAAAAAIGQk2wAAAAAAhIxkGwAAAACAkJFsAwAAAAAQMpJtAAAAAABCRrINAAAAAEDISLYBAAAAAAgZyTYAAAYsXLjQJSUluSlTphR5Gccdd5zr06ePS5RjjjnGvf76686yQYMGudatWxfovkOGDHFdunTZ4+sEACiZSLYBACikSy65xJ155pklLhHP7zJ06FD37rvvurvuuish6/jhhx+6P/74w1144YVub3HppZe6n3/+2Y0bNy7RqwIASIDURDwpAAAIT7169dzvv/8e/fuhhx5yo0ePdl988UX0uoyMDJeWlpawzf7EE0+4nj17uuTkvec4f9myZd1FF13kX3vHjh0TvToAgGK29/zHAwCgmHzzzTfuyCOPdOXKlXO1a9d2/fv3dzt37ozerkS4Q4cOrnLlyq5atWru9NNPd/Pmzcu2jJ9++skdeuihrnz58u6II45wkydPzvP5UlJSXK1ataKXihUrutTU1GzXKdHOOYy8QYMG7u6773YXX3yxf0z9+vV9BXrVqlXujDPO8Ne1bNnSTZw4Mdvzffvttz551DKV6F933XVu8+bNea6flvfll19mG1IdiUT8kOz999/fb6c6der45QS2bdvmbrrpJrfffvu5ChUquLZt27qvv/4623K/++47/5r22WcfV6VKFde5c2e3du3a6OO1vJo1a/ptqO09YcKE6GO1LFX8x4wZ47evlnH00Ue7X3/9Ndtz3H///W7fffd1lSpVcr169XJbt27NdruWo32tddT+bN++vVu0aFH0dr1mbdMtW7bkuX0AAKUTyTYAACFatmyZO/XUU12bNm3c1KlT3bPPPuteeukln9QGlJjeeOONPolVsqdq71lnneWysrL87Zs2bfIJ+EEHHeQmTZrkk1IlnnvCo48+6hNEJfOnnXaa6969u0++//GPf/gh0I0bN/Z/KzkWHRQ4+eST3TnnnOOmTZvmRowY4ZPv3r175/kcul3JbPPmzaPXvfPOO/65n3vuOTdnzhz3/vvvuxYtWkRv1/LGjx/v3nzzTf885513nn9e3Vc0d71Tp05+G+l+eg4ltpmZmf72fv36+ed49dVX/es44IADfDL+559/Zlu3W2+91T388MN+X+gAhYZ+B0aOHOm3/b333utv14GTZ555Jnq7DqBoOsGxxx7r11HrccUVV/gkPqBEXvf78ccfd3NPAQDMiQAAgELp0aNH5Iwzzoh72y233BJp2rRpJCsrK3rd008/HalYsWIkMzMz7mNWrVqlTDbyyy+/+L+fe+65SLVq1SJbtmyJ3ufZZ5/195k8efIu12/gwIGRVq1a5br+2GOPjVx//fXRv+vXrx/5xz/+Ef37999/989x++23R68bP368v063Sa9evSJXXHFFtuWOGzcukpycnG19Yz366KORRo0aZbvu4Ycfjhx44IGR7du357r/okWLIikpKZFly5Zlu75Tp06RAQMG+N+7du0aad++fdzn27RpU6RMmTKR4cOHR6/T89SpUycyePBg//dXX33lX9cXX3wRvc/HH3/srwteR7t27SJXX311tmW3bds2um3XrFnj7//1119H8lOlSpXI0KFD870PAKD0obINAECIZs2a5dq1a5etuqnKsarVS5cu9X+rOtu1a1fXqFEjl56e7odzy+LFi6PL0PBtDX8OaJl7gp4noOHSElthDq5buXKl/6lqvZqtaYh5cFHFWFX5BQsWxH0ODaGOfS2iSrWu1za4/PLL3XvvvRcdav/LL7/4CvWBBx6Y7Xk0PD8Ybh9UtuPRfXbs2OG3e6BMmTJ+uLe2bV6vX5Xr2Neq+2r4eqzY/VC1alXfLE+vX1X1xx9/PNvc+YCG2//1119x1xUAUHrRIA0AgGKmxEzzo1944QU/V1mJ6iGHHOK2b99e7PtCSWggOEAQ77rYIe7//Oc/s82vDmj+dTzVq1ePzqUOaK635keridvnn3/urr76avfggw/6hFrPoXnoGkKvn7GUdEtYzd7ye60F8corr/htoXn4GlJ/2223+ddz1FFHRe+joes1atQIZX0BAHZQ2QYAIESal6y5u8Ec56CRlxps1a1b161Zs8YnmUrKVJnV/XMmorpOc4Bjm3H98MMPJWI/HXbYYW7mzJl+DnTOi7pvx6NGbytWrMj1OpUw68CDunWr0Zi2m6raur8q26ow53wONXsLKtKa7x6P5plrXbTdA6p0q0Ga5ngXlPZDzrnW8faD1nfAgAHu+++/9wdNYs8lriq79qPuAwDYu5BsAwBQBOvXr/dDmWMvS5Ys8RVa/bz22mvd7Nmz3QcffOAGDhzoG6KpEZq6ZqsD+fPPP+/mzp3ru3Trtlg6XZSqrBpercT2k08+8afzKgluvvlmn1SqgZles4bE6zXm1yBNiaaq27HJr4aiq3Hc9OnT3fz5891//vMfn3yr4q/h4926dfON2XRucA1PV3f2++67z3388cf+8UpulTxre+vAhLa1mtGtXr3adwa/6qqr3L/+9S9fcdY21LbUUG51FC+o66+/3r388su+ev3bb7/5/Thjxozo7VovrYcOEqgD+Weffea3R2wjOJ1jW0PldQAAALB3YRg5AABFoEpszmqlErkXX3zRJ8dK9Fq1auXn9ep6VbJFCbc6bGvosaqgTZs29ZVdncIqdqj0qFGj3JVXXumfQ9XYBx54wHcATzRVlDXUW128dfovVfCVSF5wwQV5PkZDwXWO7eHDh/su66LTZOm0WjrQoCq25onrNetAhCjBVQf3vn37+g7vStY1NDt4vBJyJbe33HKLn4utRF3zqzUXXrRsDQdXd/WNGzf6ruCffvqpP9hRUHpNqkyrs7mq09r+SuK1HFGHdSX56niuEQua833NNdf4YfaBN954wyf6AIC9T5K6pCV6JQAAQOmmYeQHH3ywPw2Xqtd7A1XBTzjhBF8Vz8jISPTqAACKGcPIAQDAHqe51ho2HnRc3xuoM/mwYcNItAFgL0VlGwAAAACAkFHZBgAAAAAgZCTbAAAAAACEjGQbAAAAAICQkWwDAAAAABAykm0AAAAAAEJGsg0AAAAAQMhItgEAAAAACBnJNgAAAAAAISPZBgAAAAAgZCTbAAAAAACE7P8BKek8x4j23pwAAAAASUVORK5CYII=", "text/plain": [ "
" ] @@ -252,11 +252,11 @@ "output_type": "stream", "text": [ "--- to_polars benchmark ---\n", - "TDC to_polars: 6.66ms\n", + "TDC to_polars: 4.39ms\n", "Fast-Forward tracking access: 0.01ms\n", - "Kloppy to_df(engine=\"polars\"): 1076.91ms\n", + "Kloppy to_df(engine=\"polars\"): 1155.26ms\n", "\n", - "Speedup TDC vs Kloppy: 161.7x\n" + "Speedup TDC vs Kloppy: 263.2x\n" ] } ], @@ -290,7 +290,7 @@ "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAEiCAYAAADklbFjAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAATIZJREFUeJzt3QeYU1X+//EzM/Q2gID0KktvKiKKomJDVhFcXcsiiooKKtZVbGBBVESxoeiuYAE7NlZEVxALXWlKkV4EpDMg0mbu//mc/d38b0IyuRlyMwPzfj1PGJLcJPeefHOS72k3zXEcxwAAAAAAgKRLT/5TAgAAAAAAkm4AAAAAAAJETzcAAAAAAAEh6QYAAAAAICAk3QAAAAAABISkGwAAAACAgJB0AwAAAAAQEJJuAAAAAAACQtINAAAAAEBASLoBAFF98803Ji0tzXzwwQeHRQkNHDjQ7u/mzZtNQaX9036i4MbPkW7mzJnmpJNOMqVLl7bHO2fOnPzeJQA44pF0A0A+GDVqlP3B671UqVLFnH766Wb8+PG8JwXEVVddFfYelStXzrRq1coMHTrU7N271xwJli1bZq6//npTv359U6JECXuMJ598snn22WfNn3/+md+7Vyh4Yyw9Pd1Ur17dnH322bbhK5n2799vLr74YrN161bzzDPPmDfffNPUqVMnqa8BADhYkSi3AQBS5OGHHzb16tUzjuOY33//3Sbj5513nvnss8/MX//6V96HAqB48eLmX//6l/3/9u3bzYcffmjuvPNO22P4zjvvmMPZf/7zH5uE6RivvPJK07x5c7Nv3z7z/fffm7vuusv88ssv5pVXXjGFwf3332/uueeefHv9s846y74HqgtWrFhhhg8fbs444wz7HnXu3DlpDSyrVq0yr776qrn22muT8pwAgPhIugEgH+nH9PHHHx+6fs0115ijjz7avP322yTdPv3xxx92qGxQihQpYv7xj3+Ervfp08e0a9fOvPvuu+bpp5+2vZKH43Ersbv00kttT+fEiRNNtWrVQvf17dvXLF261CZ8h7M9e/aYYsWK2d5jP++zLvnlL3/5S1icdevWzbRs2dIMGzbskJNuN1Y2btxor5cvX/6Q9zfyuQEAsTG8HAAKEP0YLlmy5EE//nNycuyP72bNmtkhwErMNSR427ZtYdvVrVvXJuvqqTzhhBPstho2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X7582ArUOPLdeOONTmZmpvPNN98469evD112794d2uaGG25wateu7UycONGZNWuW0759e3txHThwwGnevLlz9tlnO3PmzHG++OILp3Llyk7//v3z6aiQSt7Vy4V4gWvGjBlOkSJFnEGDBjlLlixxRo8e7ZQqVcp56623Qts8/vjj9rvnk08+cebNm+d07drVqVevnvPnn3+Gtjn33HOdNm3aONOnT3e+//57u3L+ZZddRkEfYXr27OnUqFHDGTdunLNixQpn7NixTqVKlZx//vOfoW2Il8J91ozZs2fbi9Kbp59+2v5/1apVSYsNrXh+9NFHOz169HB+/vln+1tZddaIESPy5ZhxeCHpRkzPP/+8TaaKFStmTyE2bdo0SquQ0RdXtMvIkSND2+gLq0+fPvY0Gvry6datm03MvVauXOl07tzZKVmypP2RdMcddzj79+/PhyNCfifdxAu8PvvsM9sop0bexo0bO6+88krY/TrVzwMPPGB/6GqbTp06OYsXLw7bZsuWLfaHcZkyZeypCK+++mr7AxxHlqysLFuX6HdJiRIlnPr16zv33Xdf2OmbiJfCa9KkSVF/r6ixJpmxMXfuXHuqQz2HGoGUzAN+pOkfHx3iAAAAAAAgQczpBgAAAAAgICTdAAAAAAAEhKQbAAAAAICAkHQDAAAAABAQkm4AAAAAAAJC0g0AAAAAQEBIugEAAAAAIOkGAAAAAODwQk83AAAAAAABIekGACCfrFy50qSlpZk5c+bk63swcOBA07p163x7/QceeMD07t37kJ7jm2++sWW5fft2e33UqFGmfPnyMY/xqquuMhdeeKE5nEUec24WLFhgatasaf7444+U7BsA4P8j6QYAFGgbNmwwN998s6lfv74pXry4qVWrljn//PPN119/bQ53Opb169eb5s2bB/YadevWtYlZrIuSzzvvvDPfylPv77PPPmvuu+++Q3qek046yZZlZmamr+31mkrMC4umTZuaE0880Tz99NP5vSsAUOgUye8dAAAgt57gk08+2fZYDhkyxLRo0cLs37/fTJgwwfTt29csWrSoQBee9rVo0aIx78/IyDBVq1YNdB9mzpxpsrOz7f+nTJliLrroIrN48WJTrlw5e1vJkiVNmTJl7CU//Otf/7IJc506dQ7peYoVK5ZQWfpNzo8kV199tbnuuutM//79TZEi/AQEgFShpxsAUGD16dPH9sbOmDHDJot/+ctfTLNmzcztt99upk2bFtpu9erVpmvXrjZxVDJ5ySWXmN9///2gocVvvvmm7flVwnXppZeanTt32vtfeeUVU716dZOTkxP2+nrOXr16ha5/8skn5thjjzUlSpSwPe8PPfSQOXDgQOh+7etLL71kLrjgAlO6dGkzaNAgs23bNnPFFVeYypUr2wS3YcOGZuTIkTGHl0+ePNmccMIJtle/WrVq5p577gl7jdNOO83ccsst5p///KepWLGiTTR1fLHodbWNLtpeqlSpErpNZRFr6PVjjz1mjj76aNvo8fDDD9v9uOuuu+zzaKiyexyuNWvW2LLX9tpG5adjzM0777xjRy546Rg1uuHWW281FSpUsPvw6quv2qHRShzLli1rjjnmGDN+/Pg8DbX2HqPrgw8+sI06eo+OOuooc+aZZ4YNxVbjQJMmTex737hxYzN8+PCw51u7dq257LLL7HHrvT/++OPN9OnTQ/crLho0aGAbBxo1amRj0Uv7rtfo1q2bKVWqlI2TTz/9NGybzz//3H4GtI+nn376QWW7atUqW5YqM+2DPit6jOuss84yW7dutTEGAEgdkm4AQIGk5OCLL76wPdpKICK583WVKCu5c5OJr776yixfvtz8/e9/D9t+2bJl5uOPPzbjxo2zF237+OOP2/suvvhis2XLFjNp0qSDXl8Js3z33XfmyiuvNP369bPzY0eMGGGHJyux9lICq8Rp/vz5NmHXfGVtrwRx4cKFNvmqVKlS1GP+7bffzHnnnWfatm1r5s6da7f997//bR599NGw7V5//XVbJkrqnnzySZsQ67iTaeLEiWbdunXm22+/tUOSBwwYYP7617/ahE6ve8MNN5jrr7/eJptur/4555xjE2KV1Q8//GAbQc4991yzb9++qK+hMlbZKEGNpGNUOanBRQn4jTfeaN8n9Yr/9NNP5uyzzzY9evQwu3fvPuRj1bB0Jcx6v/QeKYHv3r27cRzH3j969Gjz4IMP2vda96sxQu+r9lF27dplOnbsaN8/Jcp679Qo4jbifPTRRzZu7rjjDvPzzz/bclPjgTfeRI04arSYN2+ejQPFnsrIbdDQPimpViPNtddeaxtkvPRZ2bt3r33PFH9PPPFE2AgGJfxqXNH7AwBIIQcAgAJo+vTpynicsWPH5rrdl19+6WRkZDirV68O3fbLL7/Yx86YMcNeHzBggFOqVCknKysrtM1dd93ltGvXLnS9a9euTq9evULXR4wY4VSvXt3Jzs621zt16uQ89thjYa/95ptvOtWqVQtd12veeuutYducf/75ztVXXx1131esWGEfM3v2bHv93nvvdRo1auTk5OSEtnnxxRedMmXKhPajY8eOTocOHcKep23bts7dd9/txDNp0iT7etu2bQu7XeXTqlWr0PWePXs6derUCb2maL9OOeWU0PUDBw44pUuXdt5+++1QWUTu+969e52SJUs6EyZMiLo/Om7tj/e9i3aM7mv16NEjdNv69evtY6dOnRr12EaOHOlkZmbmeox6z+XHH3+0j125cmXU/WzQoIEzZsyYsNseeeQRp3379qFYKVu2rLNly5aojz/ppJOc6667Luy2iy++2DnvvPNC1/X6999/f+j6rl277G3jx4+31/v37+80bdo07Dn0nnuPuUWLFs7AgQOd3HTr1s256qqrct0GAJBc9HQDAAokt5cxHvU8akEyXbyLRqknXPe5NKxcvbAuDd3euHFj6Lp6FT/88EPbU+j2bmoIenr6/74q1XupHmV3/rMumh+rXlJvb2tkr616aDWEWj2M6v3UvOrcjqV9+/Z2qLFLc9rVk+r2KEvLli3DHhd5LMmgocnusYuGeGv4tXc+uoZhu6+r8lm6dKktY7d8NNR6z549dpRBNH/++af9qyHbkbzH6L6W9/W1P5KM427VqpXp1KmTfX71pmsou6YFiIaYa/+vueaasPdeow/c41LPc5s2bULD96O9r3ofvXTdG5+Rx6yRDJoq4R6ftm3Xrl3Y9ooVL0070H7puTUyQT3mkTQ0PRmjAwAA/rGKBgCgQNKcViWfyVosLXJBMz23dw63hu0q0f/Pf/5jh3drCO4zzzwTul+Jr4b/aohvJG/SGDkUvnPnznaurebWagi4kjsNA37qqacCO5ZkiPYaub2uyue4446zjRXR5pVH4w6zV4IbuU2813cbJpJx3Erq9d6oQeTLL780zz//vF1NXcPoNb9alIhHJr16nJvIJsOhvq8acq4h/ophHcfgwYPN0KFD7fB8l4ara245ACB16OkGABRI6jVUAvHiiy9GPbewu2CWFrfSfFddXJonrPvV4+2XEmcl1Eoa3377bbvYlRZNc+n/WvVbC3hFXrw9wtEooezZs6d56623zLBhw+zCbdHoWKZOnRrWy6+50eo91sJlBZnKZ8mSJXaRtsjyibVSuJI/9ebq/cpvSnDVQ6yGldmzZ9v5z5qLrR51LbKndQIij6tevXqhHmr1drvzr6O9r3ofvXQ9kfjUc2h+u5d3MUGXRnxovv3YsWPtHHI1FnhpTrl65QEAqUPSDQAosJRw63RXWs1bQ7+V1GmY7XPPPRcaWqtVpjUsWMPDtcCWEhMteKaFraIt0JUbPYd6CV977bXQAmouLaT1xhtv2KTsl19+sfuhYeP3339/rs+px2nVcw291uO0iJsSqFirtavxQD2T6uHX4zRMWKu1x0vs85vKSz3XWtROowRWrFhhFyTTkGfv0HgvHZPev++//97kJ/Voa3G0WbNm2ZXwlbBu2rQp9D7pPVevseLu119/tYuUaeV295zXWoRNK8FrNXQl00rQFa9qQBGt+K5F97QwnmJYj9Nr6PzofimR1mP1XGr8GTNmzEHnGddq7zqdnspenwUt1OaNNa12rsXeVOYAgNQp2N/gAIBCTaflUvKg0yOp16558+b2tEdff/21TWDcHkolp1pV+9RTT7UJhR737rvvJvx6Z5xxhu1hV1Jz+eWXh92nXnclzBq2q+HnJ554oh1+Hu/80uox1XmR1Ruq/dOQZCXr0dSoUcMOQ1fDgeYZK9HSXOJ4iX1BoGHYWjW7du3adsSAkj3tu+Z0u+cEjzUkWuWR7OHxidD+ad+1YrhOyaXy1rBsTQ1w91Gn81KirQYeNego4XV7uvUeKy7Uy6/n0DZaGd8dfq5k/Nlnn7VTCjRXXivf67l0ajS/VK5K5LUCv2Lj5Zdftg0FXmqg0tQFlb1WjdexeE9tphEcWvX9UM+JDgBITJpWU0vwMQAAAEmhnyGaK33bbbfZHmMEQ6dt0zoJ6iGPXNQNABAseroBAEC+0UgFzXE/cOAA70KANGz+3nvvJeEGgHxATzcAAAAAAAGhpxsAAAAAgICQdAMAAAAAEBCSbgAAAAAAAkLSDQAAAABAQEi6AQAAAAAICEk3AAAAAAABIekGAAAAACAgJN0AAAAAAASEpBsAAAAAABOM/wf/CXHgKwb+QgAAAABJRU5ErkJggg==", + "image/png": "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IQ9qGc8tVpRs6rWDBg2y+5BTTjnFVmKIKsHUmq7vRGWlmNa+SJ+nMlJC7aXftyow9F7ad3nLQL9x7a+0D3vhhRfMJZdcYoYPH24rK7ROojK88sorzdKlSwO9EPQbXblypd3HKeF2hzbo76+//pqjQkSv129N76V11BAQVTA8++yzgWVU0aLtUYxrKI9iQd+hKhg0Zl40rl3fl+JNr1Xirt+gKhO0fw3ddgDIFwcAcNiNHDlSZ6o5biVLlnRGjRoVtOzUqVPtc6NHjw56/Ntvv83xeJ06dexjP/74Y+CxrVu32vft27dv4LHHHnvMLjdu3Lgc65adnW3/Tpo0yS7TtGlT58CBA4HnX375Zfv4/PnzA4917NjRPjZmzJjAY0uWLLGPJSYmOr/++mvg8fHjx9vHVQauffv25ViPadOm2eXeeeedHOXWoUMHJzMzM2j5AQMG2Oe2bdvmLF682KlevbpzyimnODt37gxazt0uLR9Njx49nDJlytj31G358uXOoEGDnISEBKdly5Z2maFDh9r3e++99wKvy8jIcNq1a+eULVvWSU9PDzwe+rkFsd2pqalOnz59nFikpaXZ9+vSpUuelp8zZ45d/uabbw56/L777rOPT5w4MeYY7Nevn1O8ePGg70dxVr58eadnz56Bx2666SanWrVqzvbt24M+++qrr7bb7pah+73Wr18/R7m2atXKueiii3LdRjd+4rnNkei12k7FmF43ffp0p1OnTvbxF1980S6j+y1atHD2798f9Ftt376906hRozzFiltGH330UdDjJ5xwglO1alVnx44dgcfmzp1rf7vXX399jjK65pprcmyDWwa//PJLjt96cnKys2bNmsDjI0aMsI9rfXL7LYwdOzZHubrr4I0R6dq1q1OpUqXA/WXLltn11+NZWVlh93F79uyx8XbLLbcEPb9582YbW6GPA0B+0b0cAArRa6+9Zlt4dFM3SLXAqnVNXTFdajVV90m1yKk12r2pBa1s2bJm0qRJQe+p1k61YrvU2qXuy2pFcqllTF2gQ1vHJLRFSS1PaiFyue/tfT/RuqjF1qXPVNfPpk2b2q7lLvf/3terK6pLXYHVWqsWPr0+XDfpW265xSQlJYUt0wULFthutGqhUgueWk291IqsPCevrdxqJVQZ6qZ1Uotdu3btAi2dX3/9tW2Zu+aaawKvUc8DtSaq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n+6sXLnSGT9+vLN8+fLAMs8884yTmprqfPbZZ87cuXOdSy+91KlXr57z119/BZY5//zznVatWjm//vqrM3XqVKdhw4bONddcU0hbhVgNHDjQqVSpkvPVV185q1atcj766COnbNmyzssvvxxYhjg4OukY/sgjjzjjxo1TDYvz6aefBj1fEN97Wlqac8wxxzjXXXedPf8YO3ask5yc7IwYMeKwbivyFwe7d++253wffPCBs2TJEmfatGnOqaee6px00klB70Ec5ETSjbD0A+rTp0/gflZWllO9enVn8ODBlNhRauvWrXbnOmXKlMCOtXjx4vaEy7V48WK7jHay7o45MTHR2bx5c2CZ119/3UlJSXEOHDhQCFuB/NqzZ4/TqFEj5/vvv3c6duwYSLqJA/948MEHnQ4dOkR8Pjs72zn22GOd559/PvCY4qNkyZL2xFkWLVpk9xEzZ84MLPPNN984CQkJzoYNG+K8BSgIF110kdOzZ8+gxy6//HKbJAlx4A+hyVZBfe/Dhg1zKlSoEHSOoH1P48aND9OWIRbhKl/CVdhruTVr1tj7xEF4dC9HDhkZGea3336z3YZciYmJ9v60adMosaNUWlqa/VuxYkX7VzGgbkTeOGjSpImpXbt2IA70V12MjjnmmMAynTt3Nunp6WbhwoWHfRuQfxpGoGEC3u9biAP/+OKLL8zJJ59sunXrZoeKtG7d2rz55puB51etWmU2b94cFCOpqal2+JF3n6AupXofl5bXMWT69OmHeYuQH+3btzcTJkwwf/zxh70/d+5c89NPP5kLLrjA3icO/Kmgvnctc8YZZ5gSJUoEnTdoeNuuXbsO6zah4M4f1Q1d370QB+EVi/A4fGz79u12TJc3kRLdX7JkSaGtF+InOzvbjuE97bTTTPPmze1jOrjqoOjuRL1xoOfcZcLFifsciob333/f/P7772bmzJk5niMO/GPlypXm9ddfN/fee695+OGHbTzcdddddj/Qo0ePwG863G/eu09Qwu5VrFgxW5nHPqFoeOihh2zFqSpZk5KS7PnAwIED7ThdIQ78qaC+d/3VfAGh7+E+V6FChbhuBwqW5nzRGO9rrrnGjuMX4iA8km4AtpVzwYIFtjUD/rJu3Tpz9913m++//95OagN/V76phWrQoEH2vlq6tV8YPny4TbrhDx9++KEZPXq0GTNmjDn++OPNnDlzbKWsJkwiDgC41BvyyiuvtBPsqcIWuaN7OXKoXLmyrd0OnaVa94899lhK7Chzxx13mK+++spMmjTJ1KxZM/C4vmsNNdi9e3fEONDfcHHiPocjn7qPb9261Zx44om2RUK3KVOmmFdeecX+Xy0QxIE/aEbiZs2aBT3WtGlTe4UC7286t2OD/iqevHQ1A81kyz6haNAVKNTaffXVV9vhQ927dzf/+Mc/7BUvhDjwp4L63jlvOLoS7jVr1thKe7eVW4iD8Ei6kYO6Ep500kl2TJe3BUT327VrR4kdJVQzqYT7008/NRMnTszR3UsxULx48aA40JgrnYC7caC/8+fPDzrIujvf0JN3HJk6depkv0O1Zrk3tXaqK6n7f+LAHzS8JPSygRrXW6dOHft/7SN0MuXdJ6gbssZqevcJqqhTZY5L+xcdQzT2E0e+ffv22TG4XqqI13coxIE/FdT3rmV0SSolbd7zhsaNG9O1vIgl3Lpc3A8//GAvMelFHEQQYYI1+JwuGaYZKUeNGmVnIezVq5e9ZJh3lmoUbbfffru99MfkyZOdTZs2BW779u0LulSULiM2ceJEe8mwdu3a2VvoJcPOO+88e9mxb7/91qlSpQqXDCvivLOXC3HgD5qBtlixYvaSUcuWLXNGjx7tlC5d2nnvvfeCLhmkY8Hnn3/uzJs3z+nSpUvYSwa1bt3aXnbsp59+srPic8mwoqNHjx5OjRo1ApcM02WDdCnIBx54ILAMcXD0XsVCl//UTSnCkCFD7P/dWakL4nvXjOe6ZFj37t3tJcN0vqn9DJcMKxpxkJGRYS8VV7NmTXve5z1/9M5ITxzkRNKNiF599VWbcOl63bqEmK65iKOHdqThbrp2t0sH0t69e9vLe+ig2LVrV7tj9Vq9erVzwQUX2Ots6sSsb9++zsGDBwthixCvpJs48I8vv/zSVqSp0rVJkybOG2+8EfS8LhvUv39/e9KsZTp16uQsXbo0aJkdO3bYk2xd21mXD7zxxhvtSRyKhvT0dPv71/G/VKlSTv369e01e70n1MTB0WnSpElhzwtUEVOQ37uu8a3LE+o9VMGjZB5FIw5UERfp/FGvcxEHOSXon0it4AAAAAAAIP8Y0w0AAAAAQJyQdAMAAAAAECck3QAAAAAAxAlJNwAAAAAAcULSDQAAAABAnJB0AwAAAAAQJyTdAAAAAACQdAMAAAAAULTQ0g0AAAAAQJyQdAMAUEhWr15tEhISzJw5cwr1O3j88cfNCSecUGif379/f9OrV69Deo/Jkyfbsty9e7e9P2rUKFO+fPmI23jDDTeYyy67zBRloducm0WLFpmaNWuaP//887CsGwDgf0i6AQBHtM2bN5s777zT1K9f35QsWdLUqlXLXHLJJWbChAmmqNO2bNq0yTRv3jxun1G3bl2bmEW6Kfm87777Cq089f2+/PLL5pFHHjmk92nfvr0ty9TU1Dwtr89UYu4XzZo1M23btjVDhgwp7FUBAN8pVtgrAABAbi3Bp512mm2xfP75502LFi3MwYMHzfjx402fPn3MkiVLjujC07oWL1484vNJSUnm2GOPjes6zJw502RlZdn///LLL+aKK64wS5cuNSkpKfax5ORkU7ZsWXsrDG+99ZZNmOvUqXNI71OiRImYyjKvyfnR5MYbbzS33HKL6devnylWjFNAADhcaOkGAByxevfubVtjZ8yYYZPF4447zhx//PHm3nvvNb/++mtgubVr15ouXbrYxFHJ5JVXXmm2bNmSo2vxu+++a1t+lXBdffXVZs+ePfb5N954w1SvXt1kZ2cHfb7es2fPnoH7n3/+uTnxxBNNqVKlbMv7E088YTIzMwPPa11ff/11c+mll5oyZcqYgQMHml27dpnrrrvOVKlSxSa4jRo1MiNHjozYvXzKlCnm1FNPta361apVMw899FDQZ5x55pnmrrvuMg888ICpWLGiTTS1fZHoc7WMblpeqlatGnhMZRGp6/WgQYPMMcccYys9nnzySbse999/v30fdVV2t8O1bt06W/ZaXsuo/LSNuXn//fdtzwUvbaN6N9xzzz2mQoUKdh3efPNN2zVaiWO5cuVMw4YNzTfffJOvrtbebXR9/PHHtlJH31GlSpXMOeecE9QVW5UDTZs2td99kyZNzLBhw4Leb/369eaaa66x263v/uSTTzbTp08PPK+4aNCgga0caNy4sY1FL627PqNr166mdOnSNk6++OKLoGW+/vpr+xvQOp511lk5ynbNmjW2LFVmWgf9VvQa17nnnmt27txpYwwAcPiQdAMAjkhKDr799lvboq0EIpQ7XleJspI7N5n4/vvvzcqVK81VV10VtPyKFSvMZ599Zr766it707LPPPOMfa5bt25mx44dZtKkSTk+XwmzTJ061Vx//fXm7rvvtuNjR4wYYbsnK7H2UgKrxGn+/Pk2Ydd4ZS2vBHHx4sU2+apcuXLYbd6wYYO58MILzSmnnGLmzp1rl/3Xv/5lnn766aDl3n77bVsmSuqee+45mxBruwvSxIkTzcaNG82PP/5ouyQPGDDAXHzxxTah0+fedttt5tZbb7XJptuq37lzZ5sQq6x+/vlnWwly/vnnm4yMjLCfoTJW2ShBDaVtVDmpwkUJ+O23326/J7WK//777+a8884z3bt3N/v27TvkbVW3dCXM+r70HSmBv/zyy43jOPb50aNHm8cee8x+13pelRH6XrWOsnfvXtOxY0f7/SlR1nenShG3EufTTz+1cdO3b1+zYMECW26qPPDGm6gSR5UW8+bNs3Gg2FMZuRUaWicl1aqkufnmm22FjJd+KwcOHLDfmeLv2WefDerBoIRflSv6fgAAh5EDAMARaPr06cp4nHHjxuW63HfffeckJSU5a9euDTy2cOFC+9oZM2bY+wMGDHBKly7tpKenB5a5//77nTZt2gTud+nSxenZs2fg/ogRI5zq1as7WVlZ9n6nTp2cQYMGBX32u+++61SrVi1wX595zz33BC1zySWXODfeeGPYdV+1apV9zezZs+39hx9+2GncuLGTnZ0dWOa1115zypYtG1iPjh07Oh06dAh6n1NOOcV58MEHnWgmTZpkP2/Xrl1Bj6t8WrVqFbjfo0cPp06dOoHPFK3X6aefHrifmZnplClTxhk7dmygLELX/cCBA05ycrIzfvz4sOuj7db6eL+7cNvoflb37t0Dj23atMm+dtq0aWG3beTIkU5qamqu26jvXH777Tf72tWrV4ddzwYNGjhjxowJeuypp55y2rVrF4iVcuXKOTt27Aj7+vbt2zu33HJL0GPdunVzLrzwwsB9ff6jjz4auL9371772DfffGPv9+vXz2nWrFnQe+g7925zixYtnMcff9zJTdeuXZ0bbrgh12UAAAWLlm4AwBHJbWWMRi2PmpBMN++kUWoJ13MudStXK6xLXbe3bt0auK9WxU8++cS2FLqtm+qCnpj4/4dKtV6qRdkd/6ybxseqldTb2hraaqsWWnWhVgujWj81rjq3bWnXrp3tauzSmHa1pLotytKyZcug14VuS0FQ12R320VdvNX92jseXd2w3c9V+SxfvtyWsVs+6mq9f/9+28sgnL/++sv+VZftUN5tdD/L+/laHymI7W7VqpXp1KmTfX+1pqsru4YFiLqYa/1vuummoO9evQ/c7VLLc+vWrQPd98N9r/oevXTfG5+h26yeDBoq4W6flm3Tpk3Q8ooVLw070HrpvdUzQS3modQ1vSB6BwAA8o5ZNAAARySNaVXyWVCTpYVOaKb39o7hVrddJfr/+c9/bPdudcF96aWXAs8r8VX3X3XxDeVNGkO7wl9wwQV2rK3G1qoLuJI7dQN+4YUX4rYtBSHcZ+T2uSqfk046yVZWhBtXHo7bzV4Jbugy0T7frZgoiO1WUq/vRhUi3333nXn11VftbOrqRq/x1aJEPDTp1evcRLYgHOr3qi7n6uKvGNZ2DB482Lz44ou2e75L3dU1thwAcPjQ0g0AOCKp1VAJxGuvvRb22sLuhFma3ErjXXVzaZywnleLd14pcVZCraRx7NixdrIrTZrm0v8167cm8Aq9eVuEw1FC2aNHD/Pee++ZoUOH2onbwtG2TJs2LaiVX2Oj1XqsicuOZCqfZcuW2UnaQssn0kzhSv7Umqvvq7ApwVULsSpWZs+ebcc/ayy2WtQ1yZ7mCQjdrnr16gVaqNXa7Y6/Dve96nv00v1Y4lPvofHtXt7JBF3q8aHx9uPGjbNjyFVZ4KUx5WqVBwAcPiTdAIAjlhJuXe5Ks3mr67eSOnWzfeWVVwJdazXLtLoFq3u4JthSYqIJzzSxVbgJunKj91Ar4b///e/ABGouTaT1zjvv2KRs4cKFdj3UbfzRRx/N9T31Os16rq7Xep0mcVMCFWm2dlUeqGVSLfx6nboJa7b2aIl9YVN5qeVak9qpl8CqVavshGTq8uztGu+lbdL399NPP5nCpBZtTY42a9YsOxO+EtZt27YFvid952o1Vtz98ccfdpIyzdzuXvNak7BpJnjNhq5kWgm64lUVKKIZ3zXpnibGUwzrdfoMXR89r5RI67V6L1X+jBkzJsd1xjXbuy6np7LXb0ETtXljTbOda7I3lTkA4PA5so/gAABf02W5lDzo8khqtWvevLm97NGECRNsAuO2UCo51azaZ5xxhk0o9LoPPvgg5s87++yzbQu7kpprr7026Dm1uithVrdddT9v27at7X4e7frSajHVdZHVGqr1U5dkJevh1KhRw3ZDV8WBxhkr0dJY4miJ/ZFA3bA1a3bt2rVtjwEle1p3jel2rwkeqUu0yqOgu8fHQuunddeM4bokl8pb3bI1NMBdR13OS4m2KnhUoaOE123p1nesuFArv95Dy2hmfLf7uZLxl19+2Q4p0Fh5zXyv99Kl0fJK5apEXjPwKzaGDx9uKwq8VEGloQsqe80ar23xXtpMPTg06/uhXhMdABCbBM2mFuNrAAAACoROQzRW+h//+IdtMUZ86LJtmidBLeShk7oBAOKLlm4AAFBo1FNBY9wzMzP5FuJI3eYffvhhEm4AKAS0dAMAAAAAECe0dAMAAAAAECck3QAAAAAAxAlJNwAAAAAAcULSDQAAAABAnJB0AwAAAAAQJyTdAAAAAADECUk3AAAAAABxQtINAAAAAECckHQDAAAAAGDi4/8A7Cw5AcHjIWIAAAAASUVORK5CYII=", "text/plain": [ "
" ] diff --git a/examples/04-udfs.ipynb b/examples/04-udfs.ipynb index fb992ac..6d564ae 100644 --- a/examples/04-udfs.ipynb +++ b/examples/04-udfs.ipynb @@ -18,7 +18,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Loaded 145967 frames.\n" + "Loaded 7500 frames.\n" ] } ], @@ -68,7 +68,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Expression Engine time: 0.4595s\n" + "Expression Engine time: 0.1285s\n" ] } ], @@ -97,7 +97,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Polars Vectorized time: 0.4631s\n" + "Polars Vectorized time: 0.0992s\n" ] } ], @@ -123,7 +123,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Numpy Vectorized time: 0.0394s\n" + "Numpy Vectorized time: 0.0076s\n" ] } ], @@ -161,7 +161,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Optimized Rust Kernel time: 0.0359s\n" + "Optimized Rust Kernel time: 0.0046s\n" ] } ], @@ -222,7 +222,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Native Polars Batch time: 0.8644s\n" + "Native Polars Batch time: 0.0964s\n" ] } ], diff --git a/examples/05-lazy.ipynb b/examples/05-lazy.ipynb index b25ecbd..8c24895 100644 --- a/examples/05-lazy.ipynb +++ b/examples/05-lazy.ipynb @@ -126,7 +126,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Collected 1500 frames in 0.0165s.\n", + "Collected 1500 frames in 0.0169s.\n", "shape: (5, 6)\n", "┌──────────┬────────┬────────┬──────────┬────────┬──────────┐\n", "│ frame_id ┆ ball/z ┆ ball/y ┆ ball/vel ┆ ball/x ┆ ball/acc │\n", diff --git a/examples/06-skeletal.ipynb b/examples/06-skeletal.ipynb index 36dbb21..be4ab76 100644 --- a/examples/06-skeletal.ipynb +++ b/examples/06-skeletal.ipynb @@ -58,9 +58,9 @@ "output_type": "stream", "text": [ "Found 95 minutes of data.\n", - "Loaded 132917 frames in 29.43 seconds.\n", - "Memory used: 2692.30 MB\n", - "Total columns: 3552\n" + "Loaded 132917 frames in 28.34 seconds.\n", + "Memory used: 2283.92 MB\n", + "Total columns: 3556\n" ] } ], @@ -139,8 +139,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "Loaded 118 columns for player #10 in 0.4122 seconds.\n", - "Memory used for selection: -54.38 MB\n" + "Loaded 118 columns for player #10 in 0.2992 seconds.\n", + "Memory used for selection: -214.36 MB\n" ] } ], @@ -366,42 +366,42 @@ "\n", "\n", "
\n", - " \n", + " \n", "
\n", - " \n", + " oninput=\"anim2bc8c1256cb14c3ea03b2eaed2bff8c8.set_frame(parseInt(this.value));\">\n", "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", "
\n", - "
\n", - " \n", - " \n", - " Once\n", + " \n", - " \n", - " Loop\n", + " \n", - " \n", + " \n", "
\n", "
\n", "
\n", @@ -411,9 +411,9 @@ " /* Instantiate the Animation class. */\n", " /* The IDs given should match those used in the template above. */\n", " (function() {\n", - " var img_id = \"_anim_imga73aa44715804f77a7b32c6bca3f75d9\";\n", - " var slider_id = \"_anim_slidera73aa44715804f77a7b32c6bca3f75d9\";\n", - " var loop_select_id = \"_anim_loop_selecta73aa44715804f77a7b32c6bca3f75d9\";\n", + " var img_id = \"_anim_img2bc8c1256cb14c3ea03b2eaed2bff8c8\";\n", + " var slider_id = \"_anim_slider2bc8c1256cb14c3ea03b2eaed2bff8c8\";\n", + " var loop_select_id = \"_anim_loop_select2bc8c1256cb14c3ea03b2eaed2bff8c8\";\n", " var frames = new Array(150);\n", " \n", " frames[0] = \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAMgCAYAAACwGEg9AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90\\\n", @@ -226848,7 +226848,7 @@ " /* set a timeout to make sure all the above elements are created before\n", " the object is initialized. */\n", " setTimeout(function() {\n", - " anima73aa44715804f77a7b32c6bca3f75d9 = new Animation(frames, img_id, slider_id, 40.0,\n", + " anim2bc8c1256cb14c3ea03b2eaed2bff8c8 = new Animation(frames, img_id, slider_id, 40.0,\n", " loop_select_id);\n", " }, 0);\n", " })()\n", diff --git a/examples/08-idsse-warehouse.ipynb b/examples/08-idsse-warehouse.ipynb index c428c04..1e3a332 100644 --- a/examples/08-idsse-warehouse.ipynb +++ b/examples/08-idsse-warehouse.ipynb @@ -138,16 +138,16 @@ "text": [ "\n", "Processing match DFL-MAT-J03WMX...\n", - " ✅ Parsed in 1.48s, Saved in 1.36s\n", - " ✅ Rows: 145,967\n", + " ✅ Parsed in 0.90s, Saved in 0.22s\n", + " ✅ Rows: 7,500\n", "\n", "Processing match DFL-MAT-J03WN1...\n", - " ✅ Parsed in 1.22s, Saved in 1.26s\n", - " ✅ Rows: 141,561\n", + " ✅ Parsed in 0.92s, Saved in 0.17s\n", + " ✅ Rows: 7,500\n", "\n", "Processing match DFL-MAT-J03WOH...\n", - " ✅ Parsed in 1.19s, Saved in 1.08s\n", - " ✅ Rows: 137,214\n" + " ✅ Parsed in 0.75s, Saved in 0.18s\n", + " ✅ Rows: 7,500\n" ] } ], @@ -198,11 +198,11 @@ "output_type": "stream", "text": [ "Warehouse Summary:\n", - " - Match DFL-MAT-J03WMX: 165,259 rows\n", - " - Match DFL-MAT-J03WN1: 162,273 rows\n", - " - Match DFL-MAT-J03WOH: 157,790 rows\n", + " - Match DFL-MAT-J03WMX: 7,500 rows\n", + " - Match DFL-MAT-J03WN1: 7,500 rows\n", + " - Match DFL-MAT-J03WOH: 7,500 rows\n", "\n", - "Total rows in warehouse: 485,322\n" + "Total rows in warehouse: 22,500\n" ] } ], @@ -242,7 +242,7 @@ "output_type": "stream", "text": [ "Loading data for DFL-MAT-J03WMX...\n", - " ✅ Loaded and pivoted 165259 rows in 1.0861s\n" + " ✅ Loaded and pivoted 7500 rows in 0.0779s\n" ] }, { @@ -255,10 +255,10 @@ " white-space: pre-wrap;\n", "}\n", "\n", - "shape: (5, 241)
competition/idseason/idmatch/idframe_idperiodtimestampball_statusball_poss_team_idteams/none/players/DFL-OBJ-002G4A/player/idteams/none/players/DFL-OBJ-002G4A/xteams/none/players/DFL-OBJ-002G4A/yteams/none/players/DFL-OBJ-002G4A/zteams/none/players/DFL-OBJ-002G4A/accteams/none/players/DFL-OBJ-002G4A/velteams/none/players/DFL-OBJ-0000IA/player/idteams/none/players/DFL-OBJ-0000IA/xteams/none/players/DFL-OBJ-0000IA/yteams/none/players/DFL-OBJ-0000IA/zteams/none/players/DFL-OBJ-0000IA/accteams/none/players/DFL-OBJ-0000IA/velteams/none/players/DFL-OBJ-0002HE/player/idteams/none/players/DFL-OBJ-0002HE/xteams/none/players/DFL-OBJ-0002HE/yteams/none/players/DFL-OBJ-0002HE/zteams/none/players/DFL-OBJ-0002HE/accteams/none/players/DFL-OBJ-0002HE/velteams/none/players/DFL-OBJ-0027V2/player/idteams/none/players/DFL-OBJ-0027V2/xteams/none/players/DFL-OBJ-0027V2/yteams/none/players/DFL-OBJ-0027V2/zteams/none/players/DFL-OBJ-0027V2/accteams/none/players/DFL-OBJ-0027V2/velreferees/DFL-OBJ-000141/player/idreferees/DFL-OBJ-000141/xreferees/DFL-OBJ-000141/yreferees/DFL-OBJ-000141/zreferees/DFL-OBJ-000141/accreferees/DFL-OBJ-00011J/velteams/none/players/DFL-OBJ-J015S4/player/idteams/none/players/DFL-OBJ-J015S4/xteams/none/players/DFL-OBJ-J015S4/yteams/none/players/DFL-OBJ-J015S4/zteams/none/players/DFL-OBJ-J015S4/accteams/none/players/DFL-OBJ-J015S4/velteams/away/player/idteams/away/xteams/away/yteams/away/zteams/away/accteams/away/velteams/none/players/DFL-OBJ-J01BGM/player/idteams/none/players/DFL-OBJ-J01BGM/xteams/none/players/DFL-OBJ-J01BGM/yteams/none/players/DFL-OBJ-J01BGM/zteams/none/players/DFL-OBJ-J01BGM/accteams/none/players/DFL-OBJ-J01BGM/velteams/none/players/DFL-OBJ-0027G0/player/idteams/none/players/DFL-OBJ-0027G0/xteams/none/players/DFL-OBJ-0027G0/yteams/none/players/DFL-OBJ-0027G0/zteams/none/players/DFL-OBJ-0027G0/accteams/none/players/DFL-OBJ-0027G0/velteams/none/players/DFL-OBJ-0002AU/player/idteams/none/players/DFL-OBJ-0002AU/xteams/none/players/DFL-OBJ-0002AU/yteams/none/players/DFL-OBJ-0002AU/zteams/none/players/DFL-OBJ-0002AU/accteams/none/players/DFL-OBJ-0002AU/velteams/home/jersey_colourteams/home/nameteams/home/idteams/away/jersey_colourteams/away/nameteams/away/id
strstrstri64catdatetime[μs]boolstrstrf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strstrstrstrstrstr
nullnull"DFL-MAT-J03WMX"10000"first_half"2023-05-27 13:30:12.600null"DFL-CLU-00000G""DFL-OBJ-002G4A"10.39-3.39null0.00.0"DFL-OBJ-0000IA"-1.5312.68null0.00.0"DFL-OBJ-0002HE"47.3699990.25null0.00.0nullnullnullnullnullnull"referee:DFL-OBJ-000141"1.218.15null0.00.0nullnullnullnullnullnull"team:away"nullnullnullnullnull"DFL-OBJ-J01BGM"1.413.05null0.00.0"DFL-OBJ-0027G0"-12.8-18.85null0.00.0"DFL-OBJ-0002AU"0.11-21.870001null0.00.0"#F4F5F0""1. FC Köln""DFL-CLU-000008""#25282A""FC Bayern München""DFL-CLU-00000G"
nullnull"DFL-MAT-J03WMX"10001"first_half"2023-05-27 13:30:12.640null"DFL-CLU-00000G""DFL-OBJ-002G4A"10.28-3.39null1.2210.72"DFL-OBJ-0000IA"-1.4512.68null0.011.34"DFL-OBJ-0002HE"47.340.26null0.423.56nullnullnullnullnullnull"referee:DFL-OBJ-000141"1.2818.129999null0.670.19nullnullnullnullnullnull"team:away"nullnullnullnullnull"DFL-OBJ-J01BGM"1.3913.05null0.150.7"DFL-OBJ-0027G0"-12.83-18.860001null0.03.04"DFL-OBJ-0002AU"0.08-21.870001null0.01.73"#F4F5F0""1. FC Köln""DFL-CLU-000008""#25282A""FC Bayern München""DFL-CLU-00000G"
nullnull"DFL-MAT-J03WMX"10002"first_half"2023-05-27 13:30:12.680null"DFL-CLU-00000G""DFL-OBJ-002G4A"10.16-3.39null1.1310.9"DFL-OBJ-0000IA"-1.2712.65null0.012.8"DFL-OBJ-0002HE"47.2999990.26null0.373.62nullnullnullnullnullnull"referee:DFL-OBJ-000141"1.3418.09null0.610.32nullnullnullnullnullnull"team:away"nullnullnullnullnull"DFL-OBJ-J01BGM"1.3913.05null0.670.76"DFL-OBJ-0027G0"-12.88-18.860001null0.03.17"DFL-OBJ-0002AU"0.04-21.860001null0.02.03"#F4F5F0""1. FC Köln""DFL-CLU-000008""#25282A""FC Bayern München""DFL-CLU-00000G"
nullnull"DFL-MAT-J03WMX"10003"first_half"2023-05-27 13:30:12.720null"DFL-CLU-00000G""DFL-OBJ-002G4A"10.03-3.39null1.0311.06"DFL-OBJ-0000IA"-1.0712.64null0.014.26"DFL-OBJ-0002HE"47.2599980.26null0.343.68nullnullnullnullnullnull"referee:DFL-OBJ-000141"1.4118.059999null0.550.47nullnullnullnullnullnull"team:away"nullnullnullnullnull"DFL-OBJ-J01BGM"1.3813.06null1.190.9"DFL-OBJ-0027G0"-12.91-18.870001null0.03.31"DFL-OBJ-0002AU"0.01-21.85null0.02.57"#F4F5F0""1. FC Köln""DFL-CLU-000008""#25282A""FC Bayern München""DFL-CLU-00000G"
nullnull"DFL-MAT-J03WMX"10004"first_half"2023-05-27 13:30:12.760null"DFL-CLU-00000G""DFL-OBJ-002G4A"9.91-3.39null0.9211.19"DFL-OBJ-0000IA"-0.8812.61null0.015.7"DFL-OBJ-0002HE"47.2200010.27null0.323.73nullnullnullnullnullnull"referee:DFL-OBJ-000141"1.4918.02null0.470.63nullnullnullnullnullnull"team:away"nullnullnullnullnull"DFL-OBJ-J01BGM"1.3713.06null1.671.11"DFL-OBJ-0027G0"-12.94-18.870001null0.03.47"DFL-OBJ-0002AU"-0.01-21.85null0.01.87"#F4F5F0""1. FC Köln""DFL-CLU-000008""#25282A""FC Bayern München""DFL-CLU-00000G"
" + "shape: (5, 181)
competition/idseason/idmatch/idframe_idperiodtimestampball_poss_team_idball_statusteams/none/players/DFL-OBJ-0002BO/player/idteams/none/players/DFL-OBJ-0002BO/xteams/none/players/DFL-OBJ-0002BO/yteams/none/players/DFL-OBJ-0002BO/zteams/none/players/DFL-OBJ-0002BO/accteams/none/players/DFL-OBJ-0002BO/velteams/none/players/DFL-OBJ-J017RE/player/idteams/none/players/DFL-OBJ-J017RE/xteams/none/players/DFL-OBJ-J017RE/yteams/none/players/DFL-OBJ-J017RE/zteams/none/players/DFL-OBJ-J017RE/accteams/none/players/DFL-OBJ-J017RE/velteams/none/players/DFL-OBJ-J01APO/player/idteams/none/players/DFL-OBJ-J01APO/xteams/none/players/DFL-OBJ-J01APO/yteams/none/players/DFL-OBJ-J01APO/zteams/none/players/DFL-OBJ-J01APO/accteams/none/players/DFL-OBJ-J01APO/velteams/home/player/idteams/home/xteams/home/yteams/home/zteams/home/accteams/home/velteams/none/players/DFL-OBJ-0000IA/player/idteams/none/players/DFL-OBJ-0000IA/xteams/none/players/DFL-OBJ-0000IA/yteams/none/players/DFL-OBJ-0000IA/zteams/none/players/DFL-OBJ-0000IA/accteams/none/players/DFL-OBJ-0027AX/velteams/none/players/DFL-OBJ-0027G6/player/idteams/none/players/DFL-OBJ-0027G6/xteams/none/players/DFL-OBJ-0027G6/yteams/none/players/DFL-OBJ-0027G6/zteams/none/players/DFL-OBJ-0027G6/accteams/none/players/DFL-OBJ-0027G6/velteams/none/players/DFL-OBJ-0002DR/player/idteams/none/players/DFL-OBJ-0002DR/xteams/none/players/DFL-OBJ-0002DR/yteams/none/players/DFL-OBJ-0002DR/zteams/none/players/DFL-OBJ-0002DR/accteams/none/players/DFL-OBJ-0002DR/velreferees/DFL-OBJ-000141/player/idreferees/DFL-OBJ-000141/xreferees/DFL-OBJ-000141/yreferees/DFL-OBJ-000141/zreferees/DFL-OBJ-000141/accreferees/DFL-OBJ-000141/velteams/none/players/DFL-OBJ-J01BGM/player/idteams/none/players/DFL-OBJ-J01BGM/xteams/none/players/DFL-OBJ-J01BGM/yteams/none/players/DFL-OBJ-J01BGM/zteams/none/players/DFL-OBJ-J01BGM/accteams/none/players/DFL-OBJ-J01BGM/velteams/none/players/DFL-OBJ-0027G0/player/idteams/none/players/DFL-OBJ-0027G0/xteams/none/players/DFL-OBJ-0027G0/yteams/none/players/DFL-OBJ-0027G0/zteams/none/players/DFL-OBJ-0027G0/accteams/none/players/DFL-OBJ-0027G0/velteams/home/jersey_colourteams/home/nameteams/home/idteams/away/jersey_colourteams/away/nameteams/away/id
strstrstri64catdatetime[μs]strboolstrf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strf32f32f32f64f64strstrstrstrstrstr
nullnull"DFL-MAT-J03WMX"10000"first_half"2023-05-27 13:30:12.600"DFL-CLU-00000G"null"DFL-OBJ-0002BO"17.2620.85null0.00.0"DFL-OBJ-J017RE"-20.779.07null0.00.0"DFL-OBJ-J01APO"-13.825.719999null0.00.0"team:home"nullnullnullnullnull"DFL-OBJ-0000IA"-1.5312.68null0.00.0"DFL-OBJ-0027G6"0.78-0.54null0.00.0"DFL-OBJ-0002DR"-46.730.18null0.00.0"referee:DFL-OBJ-000141"1.218.15null0.00.0"DFL-OBJ-J01BGM"1.413.05null0.00.0"DFL-OBJ-0027G0"-12.8-18.85null0.00.0"#F4F5F0""1. FC Köln""DFL-CLU-000008""#25282A""FC Bayern München""DFL-CLU-00000G"
nullnull"DFL-MAT-J03WMX"10001"first_half"2023-05-27 13:30:12.640"DFL-CLU-00000G"null"DFL-OBJ-0002BO"17.2320.879999null1.084.43"DFL-OBJ-J017RE"-20.779.11null2.113.1"DFL-OBJ-J01APO"-13.7825.74null1.232.59"team:home"nullnullnullnullnull"DFL-OBJ-0000IA"-1.4512.68null0.08.94"DFL-OBJ-0027G6"0.85-0.56null2.347.74"DFL-OBJ-0002DR"-46.7099990.18null1.012.21"referee:DFL-OBJ-000141"1.2818.129999null0.676.96"DFL-OBJ-J01BGM"1.3913.05null0.150.7"DFL-OBJ-0027G0"-12.83-18.860001null0.03.04"#F4F5F0""1. FC Köln""DFL-CLU-000008""#25282A""FC Bayern München""DFL-CLU-00000G"
nullnull"DFL-MAT-J03WMX"10002"first_half"2023-05-27 13:30:12.680"DFL-CLU-00000G"null"DFL-OBJ-0002BO"17.1820.91null1.074.6"DFL-OBJ-J017RE"-20.779.14null2.063.4"DFL-OBJ-J01APO"-13.7525.76null1.082.76"team:home"nullnullnullnullnull"DFL-OBJ-0000IA"-1.2712.65null0.09.39"DFL-OBJ-0027G6"0.94-0.59null2.288.08"DFL-OBJ-0002DR"-46.680.18null0.982.35"referee:DFL-OBJ-000141"1.3418.09null0.617.05"DFL-OBJ-J01BGM"1.3913.05null0.670.76"DFL-OBJ-0027G0"-12.88-18.860001null0.03.17"#F4F5F0""1. FC Köln""DFL-CLU-000008""#25282A""FC Bayern München""DFL-CLU-00000G"
nullnull"DFL-MAT-J03WMX"10003"first_half"2023-05-27 13:30:12.720"DFL-CLU-00000G"null"DFL-OBJ-0002BO"17.1520.940001null1.034.74"DFL-OBJ-J017RE"-20.779.19null2.03.7"DFL-OBJ-J01APO"-13.7325.780001null0.912.89"team:home"nullnullnullnullnull"DFL-OBJ-0000IA"-1.0712.64null0.09.82"DFL-OBJ-0027G6"1.03-0.62null2.268.4"DFL-OBJ-0002DR"-46.6500020.18null0.962.49"referee:DFL-OBJ-000141"1.4118.059999null0.557.14"DFL-OBJ-J01BGM"1.3813.06null1.190.9"DFL-OBJ-0027G0"-12.91-18.870001null0.03.31"#F4F5F0""1. FC Köln""DFL-CLU-000008""#25282A""FC Bayern München""DFL-CLU-00000G"
nullnull"DFL-MAT-J03WMX"10004"first_half"2023-05-27 13:30:12.760"DFL-CLU-00000G"null"DFL-OBJ-0002BO"17.11000120.98null0.984.88"DFL-OBJ-J017RE"-20.7800019.22null1.923.98"DFL-OBJ-J01APO"-13.7125.809999null0.723.0"team:home"nullnullnullnullnull"DFL-OBJ-0000IA"-0.8812.61null0.010.21"DFL-OBJ-0027G6"1.12-0.65null2.298.74"DFL-OBJ-0002DR"-46.6199990.18null0.952.63"referee:DFL-OBJ-000141"1.4918.02null0.477.2"DFL-OBJ-J01BGM"1.3713.06null1.671.11"DFL-OBJ-0027G0"-12.94-18.870001null0.03.47"#F4F5F0""1. FC Köln""DFL-CLU-000008""#25282A""FC Bayern München""DFL-CLU-00000G"
" ], "text/plain": [ - "shape: (5, 241)\n", + "shape: (5, 181)\n", "┌───────────┬───────────┬───────────┬──────────┬───┬───────────┬───────────┬───────────┬───────────┐\n", "│ competiti ┆ season/id ┆ match/id ┆ frame_id ┆ … ┆ teams/hom ┆ teams/awa ┆ teams/awa ┆ teams/awa │\n", "│ on/id ┆ --- ┆ --- ┆ --- ┆ ┆ e/id ┆ y/jersey_ ┆ y/name ┆ y/id │\n", @@ -315,7 +315,8 @@ " 'jersey_colour': '#25282A',\n", " 'name': 'FC Bayern München',\n", " 'id': 'DFL-CLU-00000G'}},\n", - " 'referees': {}}}" + " 'referees': {}},\n", + " '_schema_id': 'cdf/tracking/0.2.3'}" ] }, "execution_count": 7, @@ -328,43 +329,6 @@ "metadata" ] }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'./idsse_warehouse'" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "warehouse_path" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "catalog.db \u001b[34mtracking\u001b[m\u001b[m\n" - ] - } - ], - "source": [ - "!ls idsse_warehouse/" - ] - }, { "cell_type": "code", "execution_count": 10, @@ -374,7 +338,7 @@ "name": "stdout", "output_type": "stream", "text": [ - " ✅ Loaded 11350189 rows in 0.3305s\n" + " ✅ Loaded 607500 rows in 0.0332s\n" ] }, { @@ -387,27 +351,27 @@ " white-space: pre-wrap;\n", "}\n", "\n", - "shape: (5, 16)
competition_idseason_idmatch_idframe_idperiodtimestampentity_typeentity_idxyzball_statusball_poss_team_identity_keyaccvel
strstrstri64strdatetime[μs]strstrf32f32f32boolstrstrf64f64
nullnull"DFL-MAT-J03WN1"152866"other"2023-05-27 15:09:10.480"player""DFL-OBJ-00002Z"-1.83-34.169998nullfalse"DFL-CLU-00000B""DFL-OBJ-00002Z|home"0.00.0
nullnull"DFL-MAT-J03WN1"152867"other"2023-05-27 15:09:10.520"player""DFL-OBJ-00002Z"-1.89-34.07nullfalse"DFL-CLU-00000B""DFL-OBJ-00002Z|home"0.00.0
nullnull"DFL-MAT-J03WN1"152868"other"2023-05-27 15:09:10.560"player""DFL-OBJ-00002Z"-1.93-33.990002nullfalse"DFL-CLU-00000B""DFL-OBJ-00002Z|home"0.09.58
nullnull"DFL-MAT-J03WN1"152869"other"2023-05-27 15:09:10.600"player""DFL-OBJ-00002Z"-1.98-33.889999nullfalse"DFL-CLU-00000B""DFL-OBJ-00002Z|home"0.09.58
nullnull"DFL-MAT-J03WN1"152870"other"2023-05-27 15:09:10.640"player""DFL-OBJ-00002Z"-2.03-33.790001nullfalse"DFL-CLU-00000B""DFL-OBJ-00002Z|home"0.09.58
" + "shape: (5, 16)
competition_idseason_idmatch_idframe_idperiodtimestampentity_typeentity_idxyzball_poss_team_idball_statusentity_keyaccvel
strstrstri64strdatetime[μs]strstrf32f32f32strboolstrf64f64
nullnull"DFL-MAT-J03WN1"10000"first_half"2023-05-27 13:31:06.400"team""team:away"nullnullnull"DFL-CLU-00000S"true"team:away"nullnull
nullnull"DFL-MAT-J03WN1"10001"first_half"2023-05-27 13:31:06.440"team""team:away"nullnullnull"DFL-CLU-00000S"true"team:away"nullnull
nullnull"DFL-MAT-J03WN1"10002"first_half"2023-05-27 13:31:06.480"team""team:away"nullnullnull"DFL-CLU-00000S"true"team:away"nullnull
nullnull"DFL-MAT-J03WN1"10003"first_half"2023-05-27 13:31:06.520"team""team:away"nullnullnull"DFL-CLU-00000S"true"team:away"nullnull
nullnull"DFL-MAT-J03WN1"10004"first_half"2023-05-27 13:31:06.560"team""team:away"nullnullnull"DFL-CLU-00000S"true"team:away"nullnull
" ], "text/plain": [ "shape: (5, 16)\n", - "┌──────────────┬───────────┬──────────────┬──────────┬───┬──────────────┬─────────────┬─────┬──────┐\n", - "│ competition_ ┆ season_id ┆ match_id ┆ frame_id ┆ … ┆ ball_poss_te ┆ entity_key ┆ acc ┆ vel │\n", - "│ id ┆ --- ┆ --- ┆ --- ┆ ┆ am_id ┆ --- ┆ --- ┆ --- │\n", - "│ --- ┆ str ┆ str ┆ i64 ┆ ┆ --- ┆ str ┆ f64 ┆ f64 │\n", - "│ str ┆ ┆ ┆ ┆ ┆ str ┆ ┆ ┆ │\n", - "╞══════════════╪═══════════╪══════════════╪══════════╪═══╪══════════════╪═════════════╪═════╪══════╡\n", - "│ null ┆ null ┆ DFL-MAT-J03W ┆ 152866 ┆ … ┆ DFL-CLU-0000 ┆ DFL-OBJ-000 ┆ 0.0 ┆ 0.0 │\n", - "│ ┆ ┆ N1 ┆ ┆ ┆ 0B ┆ 02Z|home ┆ ┆ │\n", - "│ null ┆ null ┆ DFL-MAT-J03W ┆ 152867 ┆ … ┆ DFL-CLU-0000 ┆ DFL-OBJ-000 ┆ 0.0 ┆ 0.0 │\n", - "│ ┆ ┆ N1 ┆ ┆ ┆ 0B ┆ 02Z|home ┆ ┆ │\n", - "│ null ┆ null ┆ DFL-MAT-J03W ┆ 152868 ┆ … ┆ DFL-CLU-0000 ┆ DFL-OBJ-000 ┆ 0.0 ┆ 9.58 │\n", - "│ ┆ ┆ N1 ┆ ┆ ┆ 0B ┆ 02Z|home ┆ ┆ │\n", - "│ null ┆ null ┆ DFL-MAT-J03W ┆ 152869 ┆ … ┆ DFL-CLU-0000 ┆ DFL-OBJ-000 ┆ 0.0 ┆ 9.58 │\n", - "│ ┆ ┆ N1 ┆ ┆ ┆ 0B ┆ 02Z|home ┆ ┆ │\n", - "│ null ┆ null ┆ DFL-MAT-J03W ┆ 152870 ┆ … ┆ DFL-CLU-0000 ┆ DFL-OBJ-000 ┆ 0.0 ┆ 9.58 │\n", - "│ ┆ ┆ N1 ┆ ┆ ┆ 0B ┆ 02Z|home ┆ ┆ │\n", - "└──────────────┴───────────┴──────────────┴──────────┴───┴──────────────┴─────────────┴─────┴──────┘" + "┌───────────────┬───────────┬──────────────┬──────────┬───┬─────────────┬────────────┬──────┬──────┐\n", + "│ competition_i ┆ season_id ┆ match_id ┆ frame_id ┆ … ┆ ball_status ┆ entity_key ┆ acc ┆ vel │\n", + "│ d ┆ --- ┆ --- ┆ --- ┆ ┆ --- ┆ --- ┆ --- ┆ --- │\n", + "│ --- ┆ str ┆ str ┆ i64 ┆ ┆ bool ┆ str ┆ f64 ┆ f64 │\n", + "│ str ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ │\n", + "╞═══════════════╪═══════════╪══════════════╪══════════╪═══╪═════════════╪════════════╪══════╪══════╡\n", + "│ null ┆ null ┆ DFL-MAT-J03W ┆ 10000 ┆ … ┆ true ┆ team:away ┆ null ┆ null │\n", + "│ ┆ ┆ N1 ┆ ┆ ┆ ┆ ┆ ┆ │\n", + "│ null ┆ null ┆ DFL-MAT-J03W ┆ 10001 ┆ … ┆ true ┆ team:away ┆ null ┆ null │\n", + "│ ┆ ┆ N1 ┆ ┆ ┆ ┆ ┆ ┆ │\n", + "│ null ┆ null ┆ DFL-MAT-J03W ┆ 10002 ┆ … ┆ true ┆ team:away ┆ null ┆ null │\n", + "│ ┆ ┆ N1 ┆ ┆ ┆ ┆ ┆ ┆ │\n", + "│ null ┆ null ┆ DFL-MAT-J03W ┆ 10003 ┆ … ┆ true ┆ team:away ┆ null ┆ null │\n", + "│ ┆ ┆ N1 ┆ ┆ ┆ ┆ ┆ ┆ │\n", + "│ null ┆ null ┆ DFL-MAT-J03W ┆ 10004 ┆ … ┆ true ┆ team:away ┆ null ┆ null │\n", + "│ ┆ ┆ N1 ┆ ┆ ┆ ┆ ┆ ┆ │\n", + "└───────────────┴───────────┴──────────────┴──────────┴───┴─────────────┴────────────┴──────┴──────┘" ] }, "execution_count": 10, @@ -455,13 +419,6 @@ " shutil.rmtree(warehouse_path)\n", "print(\"Warehouse cleaned up.\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/python/tests/test_schema.py b/python/tests/test_schema.py index fdd5491..3830b3f 100644 --- a/python/tests/test_schema.py +++ b/python/tests/test_schema.py @@ -231,8 +231,8 @@ def test_validate_missing_required(self) -> None: with pytest.raises(tdc.exceptions.SchemaError) as excinfo: fields.validate() - assert "Missing ball/z" in str(excinfo.value) - assert "Missing match/id" in str(excinfo.value) + assert "ball/z" in str(excinfo.value) + assert "match/id" in str(excinfo.value) assert fields.validate_quiet() is False def test_validate_no_players(self) -> None: diff --git a/python/tracking_data_container/__init__.py b/python/tracking_data_container/__init__.py index 530fb1b..c77bf8f 100644 --- a/python/tracking_data_container/__init__.py +++ b/python/tracking_data_container/__init__.py @@ -31,8 +31,10 @@ FrameDataField, Player, Referee, + SchemaId, Team, TrackedEntity, + list_schemas, ) from .selector import ColumnSelector, Selection @@ -71,6 +73,8 @@ "Referee", "FrameDataField", "EntityDataField", + "SchemaId", + "list_schemas", "Expr", "Attr", "Fields", diff --git a/python/tracking_data_container/_tracking_data_container.pyi b/python/tracking_data_container/_tracking_data_container.pyi index 850672d..6b46ae2 100644 --- a/python/tracking_data_container/_tracking_data_container.pyi +++ b/python/tracking_data_container/_tracking_data_container.pyi @@ -78,6 +78,25 @@ class _Fields: def referee_entities(self) -> list[_TrackedEntity]: ... def validate(self) -> None: ... def validate_quiet(self) -> bool: ... + def validate_with_schema(self, schema_id: _SchemaId) -> None: ... + +class _SchemaId: + """Internal representation of a schema identifier.""" + def __init__(self, sport: str, data_kind: str, version: str) -> None: ... + @staticmethod + def from_string(s: str) -> _SchemaId: ... + @property + def sport(self) -> str: ... + @property + def data_kind(self) -> str: ... + @property + def version(self) -> str: ... + def __str__(self) -> str: ... + def __repr__(self) -> str: ... + def __eq__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... + +def list_schemas() -> list[str]: ... class Origin: """Possible origin locations for a coordinate system.""" @@ -202,6 +221,10 @@ class _TrackingDataContainer: def num_rows(self) -> int: ... def slice(self, offset: int, length: int) -> _TrackingDataContainer: ... def validate(self) -> None: ... + def validate_with_schema(self, schema_id: _SchemaId) -> None: ... + @property + def schema_id(self) -> _SchemaId | None: ... + def with_schema_id(self, schema_id: _SchemaId) -> _TrackingDataContainer: ... def save_parquet(self, path: str) -> None: ... @staticmethod def load_parquet(path: str) -> _TrackingDataContainer: ... diff --git a/python/tracking_data_container/backends/iceberg.py b/python/tracking_data_container/backends/iceberg.py index d9522e5..b9940d5 100644 --- a/python/tracking_data_container/backends/iceberg.py +++ b/python/tracking_data_container/backends/iceberg.py @@ -272,6 +272,12 @@ def collect(self) -> TrackingDataContainer: if self._limit: wide_container = wide_container.slice(0, self._limit) + if "_schema_id" in metadata: + from ..schema import SchemaId + + schema_id = SchemaId.from_string(metadata["_schema_id"]) + wide_container._container = wide_container._container.with_schema_id(schema_id._inner) + return wide_container def add_column( diff --git a/python/tracking_data_container/catalogs/iceberg.py b/python/tracking_data_container/catalogs/iceberg.py index ef6e8d2..9ad25a6 100644 --- a/python/tracking_data_container/catalogs/iceberg.py +++ b/python/tracking_data_container/catalogs/iceberg.py @@ -240,6 +240,8 @@ def save_container( final_metadata = metadata.copy() if metadata else {} final_metadata["_entities"] = entities + if container.schema_id is not None: + final_metadata["_schema_id"] = str(container.schema_id) self._save_metadata(final_metadata, match_id) def commit_container(self, container: TrackingDataContainer) -> None: diff --git a/python/tracking_data_container/container.py b/python/tracking_data_container/container.py index 3e9402f..350494f 100644 --- a/python/tracking_data_container/container.py +++ b/python/tracking_data_container/container.py @@ -25,7 +25,7 @@ Selection as _Selection, ) from .coordinate_system import CoordinateSystem -from .schema import DataField, Fields +from .schema import DataField, Fields, SchemaId from .selector import ColumnSelector, Selection if TYPE_CHECKING: @@ -56,10 +56,15 @@ def __init__( orientation: str | OrientationMode | None = None, catalog: Catalog | None = None, validate: bool = True, + schema: str | SchemaId | None = None, ) -> None: self._catalog = catalog if isinstance(data, _TrackingDataContainer): self._container = data + if schema is not None: + if isinstance(schema, str): + schema = SchemaId.from_string(schema) + self._container = self._container.with_schema_id(schema._inner) return data_dict: dict[str, Any] @@ -74,6 +79,10 @@ def __init__( self._container = _TrackingDataContainer( data_dict, inner_cs, str(orientation) if orientation else None ) + if schema is not None: + if isinstance(schema, str): + schema = SchemaId.from_string(schema) + self._container = self._container.with_schema_id(schema._inner) if validate: self.validate() @@ -83,8 +92,24 @@ def num_rows(self) -> int: def schema(self) -> Fields: return Fields(self._container.schema) + @property + def schema_id(self) -> SchemaId | None: + """Returns the schema identifier, if set.""" + inner = self._container.schema_id + if inner is None: + return None + return SchemaId._from_inner(inner) + def validate(self) -> None: - self._container.validate() + """Validate the container against its schema. + + If a ``schema_id`` is set, validates against that schema. + Otherwise, validates against the default schema (CDF v0.2.3 tracking). + """ + if self._container.schema_id is not None: + self._container.validate_with_schema(self._container.schema_id) + else: + self._container.validate() def is_bound(self) -> bool: return self._catalog is not None diff --git a/python/tracking_data_container/schema.py b/python/tracking_data_container/schema.py index cd133b7..19855fc 100644 --- a/python/tracking_data_container/schema.py +++ b/python/tracking_data_container/schema.py @@ -6,8 +6,12 @@ from ._tracking_data_container import ( _DataField, _Fields, + _SchemaId, _TrackedEntity, ) +from ._tracking_data_container import ( + list_schemas as _list_schemas, +) class TrackedEntity: @@ -306,6 +310,83 @@ class EntityDataField(DataField): pass +class SchemaId: + """Identifies a schema by (sport, data_kind, version). + + A schema is uniquely identified by a triple of (sport, data_kind, version). + For example, ``SchemaId("cdf", "tracking", "0.2.3")`` identifies the + CDF v0.2.3 standard tracking schema. + """ + + def __init__(self, sport: str, data_kind: str, version: str) -> None: + self._inner = _SchemaId(sport, data_kind, version) + + @staticmethod + def from_string(s: str) -> SchemaId: + """Parse a schema ID from a slash-separated string. + + Parameters + ---------- + s : str + A string in the format ``"sport/data_kind/version"``. + + Returns + ------- + SchemaId + """ + instance = SchemaId.__new__(SchemaId) + instance._inner = _SchemaId.from_string(s) + return instance + + @staticmethod + def _from_inner(inner: _SchemaId) -> SchemaId: + instance = SchemaId.__new__(SchemaId) + instance._inner = inner + return instance + + @property + def sport(self) -> str: + """The sport this schema applies to.""" + return self._inner.sport + + @property + def data_kind(self) -> str: + """The kind of data (e.g., 'tracking_2d', 'skeletal', 'gps').""" + return self._inner.data_kind + + @property + def version(self) -> str: + """The version string.""" + return self._inner.version + + def __str__(self) -> str: + return str(self._inner) + + def __repr__(self) -> str: + return f"SchemaId({self})" + + def __eq__(self, other: object) -> bool: + if isinstance(other, str): + return str(self) == other + if not isinstance(other, SchemaId): + return False + return str(self) == str(other) + + def __hash__(self) -> int: + return hash(str(self)) + + +def list_schemas() -> list[str]: + """Returns a list of all registered schema identifiers. + + Returns + ------- + list[str] + A list of schema ID strings in the format ``"sport/data_kind/version"``. + """ + return _list_schemas() + + class Fields: """A collection of DataFields representing the schema of tracking data.""" @@ -374,51 +455,60 @@ def match(self, pattern: str) -> list[str]: # Standard fnmatch for simple patterns return fnmatch.filter(all_names, pattern) - def validate(self) -> None: + def validate(self, schema: str | SchemaId | None = None) -> None: + """Validate the fields against a schema. + + Parameters + ---------- + schema : str | SchemaId | None, optional + The schema to validate against. If ``None``, validates against the + default schema (CDF v0.2.3 tracking). Can be a string in + the format ``"sport/data_kind/version"`` or a :class:`SchemaId`. + + Raises + ------ + SchemaError + If required fields are missing. + SchemaWarning + If unsupported fields are present (emitted as a warning). + """ import warnings from .exceptions import SchemaError, SchemaWarning - names = self.names() - errors = [] - - # Required errors - if "ball/x" in names and "ball/z" not in names: - errors.append("Missing ball/z") - if "period" in names and "match/id" not in names: - errors.append("Missing match/id") - if any("teams/" in n for n in names) and not any("/players/" in n for n in names): - errors.append("teams/home/players missing") - - player_prefixes = set() - for n in names: - if "/players/" in n: - player_prefixes.add(n.rsplit("/", 1)[0] + "/") - for p in player_prefixes: - if f"{p}x" in names and f"{p}y" not in names: - errors.append(f"{p} missing y") - - if errors: - raise SchemaError(" AND ".join(errors)) - - # Order warnings specifically for TestFields.test_unsupported_field - # The test checks for xGoal THEN dummy in the same validate call message? - # Actually it checks record[0].message for BOTH. - all_warnings = [] - for n in names: - if n == "teams/home/xGoal": - all_warnings.append("unsupported entity attribute: teams/home/xGoal") - elif n == "dummy": - all_warnings.append("unsupported frame attribute: dummy") - - if all_warnings: - warnings.warn(" AND ".join(all_warnings), SchemaWarning) - - def validate_quiet(self) -> bool: + try: + if schema is not None: + if isinstance(schema, str): + schema = SchemaId.from_string(schema) + self._fields.validate_with_schema(schema._inner) + else: + self._fields.validate_with_schema(_SchemaId("cdf", "tracking", "0.2.3")) + except SchemaError: + raise + except Exception as e: # noqa: BLE001 + msg = str(e) + if "SchemaWarning" in type(e).__name__ or "unsupported" in msg.lower(): + warnings.warn(msg, SchemaWarning) + else: + raise SchemaError(msg) from e + + def validate_quiet(self, schema: str | SchemaId | None = None) -> bool: + """Validate the fields without raising errors. + + Parameters + ---------- + schema : str | SchemaId | None, optional + The schema to validate against. See :meth:`validate`. + + Returns + ------- + bool + ``True`` if valid, ``False`` otherwise. + """ from .exceptions import SchemaError try: - self.validate() + self.validate(schema=schema) return True except SchemaError: return False

F&xq#{=CX+Tt>@VlJ2za=;)W++>5>u$tZA` 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%0AvsE|6 z{J_S}w^7&%EUogqxv~eZA`?^+RDwrM`TO$QU)EI5%QRU$-;7pa)dT(bUvTqbW|rOUCxRNq@5kk;kjPO?^kWA&5GUU1=_Lc z6RLw!bc8_|8w%FM6Ou}KADyE7bSg7$A90`wEOGLeWW_LKrm9+$QIFqT+9T%&Z0$S| zSA$==YoU7~J5`O9IFO#FcheCV@Sb>Zaca$g#SVntr(jN)^JS}G+=57|kev_F za8-r$BO9F)w?M>mQ=1WmqLD6fD&UW#8s!?Hhag;;!IjJNsl(pI{l+tW&i zM0+yh%-S&DaVD)T>*gTfp`DO)_qVYs6H8Fm?`y^!ii&(`2zjnGOF*eBktW=qwndY8t# zyT+id|{zq;})z$eF zvfe0@d6KB?3u{)+g1%INA%q~05MXfG)KVbP#U1RVTS}S3Ol4QdKde7eF2paS@p~=P zK+AaSRm9IxVLn?T!b{>N3Ban&yy(#~yog8}cP(*c8hb)#qBo6Uk%sUj)>M5ra0U>M;YvL_Yi1#;?#yOTmF`Zv$WAt6x4| zh(EbI&*sNKu7>G%x318W{DKB@kyRu@*{{|2Zn{jE#h8!@l^gvM+(mBJ=q78`IV(}( z7M%*|xxs@w?%?h$gS&;n z^O=vYKs3Ha(RA-7=(K7F^OCcy?m1`kAQQCxivMS0pduGU(7Ctd+p54KxLZ-1(~LKG z{c_XnZsBPChsbIr$xFR~h*#fUW;dY_j|O004=ENT8PD2$kE-Ppx$iRz0VEOpurcQX zS^01k&F%L6+Fu|st}OR(htf4Z68zb{f(xv7U2!e;KOZzlpM`Z%Ej&m>St}&G^_7gF z8)2qu#RlFmu6GXy3ywgL!?k*^3y@ei2UI*W2=E^ z70_Z-3g4TL%^_3D{atoh=T0L66-HW}l6^7uc_{g2Xdr(PBAAO`93O1LBBmn{9e{b+ z@_j402n6{Cqizc66W=MF7l>o2rFL13$rfq%QZ-ux>&n9pr4i{B2&y@hx6@wazd4G; zs^p8SDIihSS1zyecj)SGl?ud1Ckkr^?FI61B^UR92o*Qtr?!3 zhLSO)G$NbSf7RJGkMVR#5Y^NoR>h*}LYQ#TRgb#CZq5Z6(+m>tsG`GycV^OJvxa>u zxTV=yW?fw^1C54au?yzMA#kdTSvGkBw%OgzGnGkIh^Z+OH>!hkt0ImlnrsuX;Hr}Nzxt=A( zXR)O}G7^6z_{FrsD9=+lJ1n2+!G+p5*(=>nDipv_t_3c;+`$Yy0=4h=q& zunGg(d+FCwrnt(|P|0|Pd9FL-v1>Mim|2&^-huzoK8zySmwgw8yTW4j$+$3vG$^VWQ@S-j zG|@10q5cO=gflu@UpJ^;m)cS9dpZxF$Hbx@oBo37pwh|+y8mo-rrYCRk0ou$_cvI| zTUnK_?3L~@zlY9$h=xp2qMZzA)e|$)y;f)5Om@+rX{3zyRkBQJbGm8 zxGS(t3Y<#BoTQceX!E8-Ni8_buUFPMQUoNLhDUtSdgFrEQ|&b2D({^8MSL@YXcr5XuX^5iCTi~fj=DYm&k^t{}OmBhIwkaqwGLDjS+7+6Y^xDC1^ueLaxGh zk6pMVg%b4%iDVloxOwAB%q1mq(!LwUy?8oIEB}G$$!Zb%^W{tB=E*20Xo&KZRxgxP zDt|@HFR?2poivzG=Qx&8PCWdF>`8{lz!h4?Iw=~K@wM{Qs@dA9s+b*Dg*qNm8Y+@L zk}#&dpKN=noAd>IF5)v^gctoh>-c4{W*gNoU&W0fi{Q)84Az2cHk}1EJu_xY5o@=w z==xZHIJ*}2WB)X+u(%l`CF>TqpZcqj#GSV!1|d^-z=IB2(WY}@^P&b%ok5cjEw#(T z%%8e?d^Ov>sy62+k+=j0#x-$QKDtKR%}~C8#_w#kl3#a-z2Ppwu=Q^SM6t(>ok9w! zEOSa+Q~UQ)@;#HK;FjrYu9c*q1&6Pja(fRMd}!4x*q9*5Pv4-ILTxky?g#wRl_TtY zR#p!|nP_Tf3XxJ}Gap%L^Q5-z?oDZrHJz?u{hev+z9v4b<7?0!844~8_Sj%!&ZsK8 zYnh~5N8iQ5!@a$B!(QTb{j4*AHe-phJPUPf&9RX3wC03E+L=xmk+3-21KJ3ftjIn% z7rUhmtxlPb7FR=lKpWo+TU@Z?JFy5_Olvoo=be3Ta_)zpft?n4bU@^;8SrC&<>6iL z_KlfP?7vJ=NKGeP5CtFL0bBGc3=d5p?-nDJ%<^^ID*7xf;5ue>KnU8as9f$4-tMEU zKu)RA3(B*(eIiKd3;OvMIX z^>k}SA0}&8Z}T>Au(^Qt**dl9j)NR+2XHV~#_o=US!nvXY4^aB$F$2#>ZTFt2SEt? zO}e@aF`30kr|f*b9$u5DI)FWgKx(0xLOg7Gu3?j5&2i=>>BAnHSt-^KrHS(`p5*#} z0`vvPQzAC|1S6tVqP!NuaFr)Moq9(mvzfzHi=^DKEz6nC@?QLU*E8;XQ()giw2`%o^6x?6|Y8113U`-Z{npg$)xzf5@iZ^WwqC(5(Pr*>*-X4CXeULl4RQ)h26M%Eb_B{jrWcaC86E@oRp@p z)jMxWo4E}l9Uk<85)kb<9OR)7TY8aI_If-K#7hRJY}LQt4=obh_dQ)t-D}{vhkb>r zyqf(KALzuYN*dk=@{CYeXVmNp6gHF0k=PTIT^DJ1#0%(E^D+VpSbhIwJQ927Wo9Sf zxSkH4(x><_8T|WMTDxL{m1is6KzJ&shq8f#DyRNRrK+0yl2Kt0v1!M zTg5XNK7LZcF2}FAJ;%9wSBe3uNByC-i=L}hmi~DdE*X(h$C7m-MArz1sjh*~*z;LL$6buP!z!m( z2MRi7^KGdhd*PiugF~|N`LJ8Irr@9BhIjlHOcSu}~fD-IJAk`Ot@Bw`v-&O|6k|rDqt} z@S#IUQMQNB6Sy!g0h-}?B~ zTjMcRsR@KIgm<<{lY2~ae+hRgIHfMzzHNDGsFZyd=?>f`W|=|mE~5RrIva8DjYsV?R9i_aG${}Vh7OEP`*4dX7>W~^* zd9r3BI_z(q8}#O!InS58;m)_U5k|?)pr)szeoVhGjL4SNp>T}|y^a{q_sl5k5=wJe zNqvFgs1W-BqepqX{c`uTQY|^EbP?W?deF)Kqxq3>J5jvBE4(eB@wCF9vsC)Qb?%1` zdZZL8vx$#3&`3suuAKWH4)MOizwkX6v#&aB&XU5y%RL+mgn5hg!)qs}fBCk5uM&Bl zOjWU&lU?8U$}5$0aork5Q)JP#cP2D#I1amZ6FUV0prEe1KxtSA%)r?U@=Dx&pwJ(V zIuL%8_DyhndHNLM0c@hR*3t@OD=RD#$_}V+Qz*D+8bj>4_lN*Q!U?cumx9R)Y^%yt z4a$+7x2@_EeiJrC`ttj-cy!&2JCo*s4v2Z?Xh)tZNV>TS&!?3bMR;A1Kw@~)+-kOe|K~__ZO2(UiD1XiH+&^nN)cFv>n(Mi}4pA^*pNUIO!w5g$Su7Z4 zjb7pd|LZy(yVNc8(mq?BW!ExP< z_8E8JvR);GxFfSCYp#}3 zf55@eSlX@?yLJ}9K+q1X{dh(@{s-W$;iSW|EbCY%B0o%+#o)!>N>i}^uFBbaNcauV z=S7_hj4uxd`SK(CJnejv^ybTO*NSjhV_ zajNaGQ|r|ztCQWwNHQ-fvmBweP0uX-xwTS-DTu`jtjVv=lomnq#1+_YpyheJpl(vS zIM+nU7oWBd&@dF`UzLTx{OJ7v0p-PVy#GG}zPer_LK<#vdp&J$g0 zq%`fSn!=p3#tEr6H|V16Q^nlGMg)k@6J1svv{tfTk6Tdo@T)oIQR%(8huI^$3Va?A zP}A&}@a8fcmewjvX{2{W^8Apg;TUJRIwR|3Vj9Q;HM1C_?-XfO%Tf^;;N~F{XS1(#hR}hs}(P`g5!XnjI_BekzpH`B$GbrzR(V zpzRti7gcHY4cBgK5B}*|n4*nW^E5M9*bFE#@Il+9N2jAG8sxiXGf|;g9@QbCj6H&V zf=dTZ;7YMZ2`5(Zmhw!!-|Iw7-oHz#wD`YKv{@IkIoY!4&eiCishz_;&vJ!|A)QzjA}Cb;(vo6fuVy^1_&S}I?~b5LJ&a#L9qNr zL3+2)LMQ=3M~ZZCD1yX-gN}u2=sh$81OaKGCUk@(5J(8c`^?HBfPFj#WrG ztO0raLw6R$K4+<2DZDyV7&wC@ylDQGC+#t2gdLChzdbBx-fBGheHU4%Qh}%C#w8z^VU7p{rbloVWPQ<+o7jIpl8QBAWKFBu5r< zz#f=7?P>kz+lVEqfo#d+#CO-fPB`}*-Z=WPV=@PtB+Oua_fYn_K=SrVtZy4?|MRG+wZEXd9nSlu%N5k+A^qz;ve$yi zXaBru`V}p|nxr*3WjaDFZM69jZ}(?QX);e(NyDx0rWhaI*9#nwgpjGqlah#9fWLdDvJHoba){sQ{@A({f%CDW^ntX z^)bPQKy?-Evi9ioZ%6U?e!BSYkgT%jHBDo~DnDh&w7)g-T(#3@sp`=rul_9$iHMLC zy`bWppEQ$_`n_poOujWy7b&y>bWMX#yeCvO(*EBTXRz#_@oXIlBm ze@0%3&QubzIvXypUL@)NbERZ5xG=~Tyylnd&jk1C;|7~vE;&c3Mr@1Q4p3Bh$`; zH6qEEoiS_5`^ndt^ewXAe}v!vqSY`hO6j zr3fPD*IW5EOvQeI{ydz*N92|8Ld@>+p@(dgDv}f8`b;_or@q|LBMSF4yD}8>#0*SO zn%6C-xrIn|(nZh0b@}3{ck85^tqxN*j-#$DP~J8)!T-^aQ8DTAu>+}!k9;`yp${IX zo18ag>ETn`K60tMFgQQaqp%JMkv9t@(I{N1JGUcT}& ziZ;7;igX)3!{Mw}+T%pAR@~eYscZV*ISdWqnsZ7MdF#^en=dJa7Sh;2Zb@4Bz{)|4 zQC@q+LiIJ3A!p)a!7nCUH|_9ZW{WV6QqDKqc(S6E#Wi*Nm-emn4T>IlD#~eP#r@!U z^hmVBc%B=20yx))n9yx&Hs7h-(j?Y43ke;jx>!5$#sBcP$^oZ|I?~r{RxNt&7U$Xx zMNFdJZHS6xB$o{@#|G}Ia3$3uBr!`Q@CZn!;|1aTUt%S=$&SM`jcoCl-nNDD$vcuF zSY7IIVp4f?d7&{dVLnaf8;*d!XOzClN8XM|NKB8G75wYeLzb^}FQ^yt+lsuMZR|ah zOqqw*^o5&en8T*adE6Xtox-=K=KT~}zv<1cXk67b>pHtcHT=}$aV{(kax<{ykKx8c zw!BR89psLao_SdoAKJ!7T~23w=t+glx#lCS@2;oWqBVBQg9$~d|AYxkIOFAf_v&{; zPHn#G`9ASR|;U+&#I{A*D0Blf%97!NwA%{fe^fg67qiUld++ zCtntPR9bf-XO8Qt%)-OrN1GQhzsIF8e-AnWVin^T=wDzbJAaY9yL~P3U3wB%Wgh1} z!RbT$wLy4>g6LLyRqwe8iE(10W5gi&iC+8AE604)#fKv+aRpi_e}l!Rqy<1&PW|jP zSUiV63J5{-;n6u-LQOgD;%(*0RGUf zs~=;+bE5L)uC5&KjFn87sB}?U^@pL(3K$BHUnxOeTpRBjHup=`3aB#D?2UIl$x2)4 z)cOdBWi564z7J!p2fvnY7_eFDiXY*&{qDPzxbT-`Zn06B-#>>iJr7$~%W!Gb5K?yL zx+a3=9<@gQpa!*M)-*QFa2EtQ(X+j!u6URLvFVp+0$(;CA09~tG>xNArhJr77o(&s<(nCJ7tIv ze!5FTn}_RkdQ-!vt0G0(D}?YexTC1uy=uD4RgROaxy=IwHvL@0Rhe`DpdcOD!}XHB zSzOUveu8*fo+DpLf-gt?fLcwN%z~%er@Q!Pzl~d#2z@9omvqj4S`;fi1d>ZZb_i^Y zp9O!&?{YMf{~fHv1#;;8=Eln2DXn{`A331kYObUv4dF_N$X=Sqf56s3Rd_H3n_xkI zc{VI>EoMrt+luGqNYi5}`h|{}n<=P=$n(gv-ZSEDpPQZ@yV{Junv^s2o9uZgtx=v; z`$UjK2qGdG=X0h`mIpMYH!AA!3$(0gqHNg-y)(!xc};N!_Cm8GJ3rHY#Wm1VY_wJz!ZZ8?o zITIJB9=8O$>YO)|y07XsnfZ6{qN(0~g1+mJs-y$YlH-w2NX_@HJpH)Mo}2wG7)1ai zrs0vLBMhpR9jqU)Q4b2`tWU->O6+~-s1Au?{$Zhk@NS)#rF%86Di`7^y*|Dy$=E9a z5I(<`*6i^6d}qbm&WVX2eEL4hM3FN;juRY*{=O}K*)fHGN)siqdFG zWpiv)-EedptZ$#X{&mW_N9{pgDNa)PllkeVy$@|Q?Go0S;Hum;9=1<=W^qlvgtS_E zon2R3M7T`C3c2{M8rBsI;3btC$+A|4E!Hl(e0yO=hSV(`|#~ zQkP#9{xH^T$#MEvQDmwplv;Dx`AuN}0=*lIO&}D(dkc|)rFMIP+Iy+#rntup#4fS7 zFKk*z{i`ppLTw%Is0`kVK`&_&nUy`K^OBl8{7sjf_0x2ZGCSK;a6d~mSptpzy(X12 zU$;l4*na}G3^SQ_SXi3yfW`7~f;n_tzP0lB)9t|i*P6UbeEfGM_KpUb1fXxM}EwWE~A}_ z1iwq?{^{CaZSV+ry2;1`EN>@dcDZcCI<)Ti+;8Ej92alx!ajz4a_wC1uP;Vc*my?{ z*z9`-pG%RFxkMlJZr-~)-UHHlyc!R=Wl+ZPj$-v3WH4kPD8KW+jXsH2N^SBrj@Ef& zT&@}|K?iv4Kz@jah#vXnvSBUZ+c(XV&WBp3YgBOV;zKv1KpZf4P0LV0;lZ?f6Uw{s3bUvLfjsA%y7yRH z3o`oPj}EQZobK7Sjq)hH<=BWmBO!7xi|5N-T5QZ{13;#;&SHxN8(RiY7_S&SOS4-I zymbOAj;<51Z+lkY-nemsdh6v@&b;Mng&}4NmDap-1#gG+^9$ga zR4@}*LezMtRbKmfV`o;Ents?!KS_z2kgM|5=A1qk*ae<8uJE7orA4iFwLg(8mNT23 zaVUGbG7un&G$3)yWU60T$Tm0YaMZif$4oC6g`}sC%FJ{|eV5k?IPY&i$IJ0zg*~aA z6_mYtJO6XRA7(iVHedYoX7(231rKLFv=%^h!|5W+^$)zSFHM@NGL5#)%;x%qH?C8Z zN~#1MHI!sdsasy`9+>p%Kvuc5AeWU4jl|++UFK{sIt#x*T1#l0v)jV~m-|9~fP~A^ zY^BaFFlno)zQX-6>R70*{QEBuPm#_xV)$$Oi(}g2evfj@JFy0ykhb|bXt!Ajw^haVE+O=@z6as`b=SCAGNe?ZShOCkwzw5 zW?3)MC)CEmOinYQkezLC!>G4HxYnMq#F?^>{XX9U5_4dai%_Q@;8ZCm=3n9=FT z!?uUNK*PrsSBxvZb|1%l9gT^Z=3}>&RiC_ysplI>wC9{XGBy6VUo^ptD$|=^KDrT; zV0`qsJZpM>%=Q=PY)#3Dv~b;Fgaun@b6tQEr@C|{v-!hZ;2X7Nb(-+y26_FAr0_*W zR7ZYhTVkWr=X?kcZB1(5 zZKkTfU2X?Hx!QS9dXy%J4SY_Tm0Qa@BfWjm)s!6q25^qwD;QDb8b6Q!Hf*c=+9%g? zWXkVidvdqjwkyW2;}_^hX$<$}t|z=6%g3J`Px|(ZpNOpP*S#_>phIdMw84kNGX zw-H$J=548R`z;dNC2h}>4{`xQ;0@%V%fPDnQt@wuqH?1`Rmn;C{!v?M#|&nkenv9; ze2ASJ{$RVM|B74DOLRKd9w5B_X6E>In~Bh8@y&CNH#U0r6b)XfPaLuxO6Ez^qw{ko z^35^MF%1`gfxx>l0lPNX*~8xb`CT~xp2SJ~8KVW5xvkkTrXxaV`ot^bP;LN>ojh^Z zKgPGVA!BYeQRlAdACHt82iFPBnc_r_G&UrEftLJT7W$17PBou=M@XxBv?=v(G}_Ks z{{mHUX$Vz^I~zX|o2jKTueS($pNvyZ)cTL?&&NUvP4(gcYGO6p+<}_4=ElU`6 zgiP0_L0^x}>+pBecD_X?meffGKR7v=xP2V+=!C4|76s0J6eI&EP5Ty7*%ef|!DGq+_9SfA7_k0H+!4Sc5j$vWj?rai zt(?%0=I4xy4|{iG&?f=^_gnwJZk5I8`~vlyc4p5qowc)NZl(7@=#O~`!t$c1!t zA9%RW9=CPJ1knHgpZ*V8Lbkj^;;6@s&Ta!34ja2)teTT|x|r$4yJ1ct6HXz2zj*X_ z39?7>(cjpp{eyjyU*kXj{PR&M_xx(9om*0PkEDD&k9;z3c()QjPutu82`_?91CFj; zMr7EOU$cV?I2+x0MiK1h^Hzs(@1(RBhtH-S0XTiHe2a@0sG?Df<7);cDbe3_`}NX* zRii?;B=PXFTuTY3kRP|Now@BL-TBSZP_tG3TS0bBENnaOM|{ z@lC4UViFW!;2io@6TZ}r-n_K=rJ6vk9Hmj;&1dZi9tm7v^1L9d`?m0u`xTv|0I5>s z#8d>y)*c97{C_xa-a{&SIZlVPoctdB3uHBI?FMNFv?ywi>*qwVtXg#F1lH72IZaCJP zXTWbZzZ9GUN|Lr9&BPlGzemLiSr-WQt1GY>_>wXDlGG;Rf>kV;o2T46ch9ISrepmi5Vqt z&!wMND6a}{@ln^-CKJ+>;`_GUBO0TGTsSLan@cHc23bPRhAnUriHf|5j*0K2j)lz* zS>kYSsIY2wcJ5v>>`J9B*O}VxNvDz<0TNMzU}3IOPD%Zz+(f0^w}G*dipFX)>Z^o( zHWnLjE--3D57<>Z>G-3i?N!L`0*nBcHDdyo=f5C3*nr~T2BJ`rGezI@yS^)HIL~N~i)abFu5&#Bd1PtPi zx&UHBtF$kSCD3+dqTTPHZe_mE&kr~0hm$7(x7sw1NV_QCHthrOvc?>0+$A#qd!tov zyu}^Mg@xzhEZ`ltXNa-nJz~;|zg2VCGN6-51Gd@iTeF3RuS9a201~+-gZU<2=>nS` z9@&N{*(+PQCSE0jxtd51cQe6GSdZXjr;TqzTvgoAr_i3B-X0jzCw{N^-6qg&yOg?w z3a~Q|Z7xR#^>}4P&<(2T>)Pe)IroLPE{MD__Ym$#6nD!cIiHIE*WS3Y{XUyr`7Gj?p7FvDD(=$c{j zhZ%)qjACNV)2o60+QN~1l~2noOUBd{T!}vQ=oiTTQB&9G+pWfzl3ZIHKU-njCl8iV>_~@w<5WLIz$Ja z_PuwdLIOEJ;!YycL1!aQX_!S}wPP8f3rpM?poZe=xKd>!}ahA`7Yz`fU^h%vS z=N|k~cfaPJY3=SkMs&WQ`Y({nvr>ohyb4+in)vD$h?Bt{T=&DVNKMcRRpJ;^3;{Xv z@MP|_IT+tv0P+EUV?5|bpnUye#rzM((_Vq-xl~~-8QTi*v7K z4{O_-y))Z{w1u-<5zj8H`oqmLK60wTA#h9LGaO}2M2WUC5I%~>NxJXRm_|+tvja)0 z2R9aE=lS7xf*NOzV*hpbcPDu{XY7kGmGC;X1Hp+aCf5igI$=rUcSz>+$Uc+ zOXNvtJl{nBM8Ze?ns(M?5NRBvs?Xmb0*g{w9U?(s^ZKhQQ`8<7B4N|#B@Uw4R<_+= z4DGOe;pI!Xye}MG3{18{gQAs`?@rQh8XM(2--D4Vu=_uMn#d7$;$4b7&opZv>z=@$ zU}@+Ni;dxkPc_A9O^RK>NaUnMC-sNLM7edk)tY{?jY?^S7kj(S zZ~M?-aBq(WmW-*FJ76=Pt`;5vKF|$zIxYIYC{i5l#rx>LCh)Gj1$Z_88%q9*D0rV) zAhBXZb!8&-nKe|-$QmkU+~}trvvo7CNyA||et(Ktqr6wRE!LHlJny4)NF0hrR_M-4 zeyuHW0_aJ30$BnBm3HJ>hC8<67YMF`vALeV#OAJ}u^-HPw0C~I@bSC!r`5ln4Tt*j zRmlSTpC6sSYVh%&KHG(LUEHptT@J=?voky zc8jA%He5arD^RLO&o?ir=|UPEC{_pz7+)ueH_d*o$jHcg>$Wm4lt?FaqE2zjG%4NM zupKof%aEWUI*)F>+NeCxd6kTPXv*n4;lJKJ+uutDS9DCM1bp!kq$)jctv)FNkjPK= zeDBLw_tR8s)`)M%J$ld>xrOM23Zd~{6aw-N`z{~art!Vh3Xw?1QS&W#rqDYf|0Pub zd6vl%(u=9LJ)i?!;k$9zG{|7Y#YNq}H-WXjHmFs}gb@|m-3`8Or*{dP!gZr%BpdCL@XaNcjBHcB*OD%!daTre7T&O4=* zEsM-7u!9do?uAS9>)E<-iDr$+)t!3y==>W#MA7$#?By)IsPIkdS#Bt=_Nh3{t+bb6 zGb6Y0U*=*KBBccX^1hfa3&EbR_dS3R3BrEZ>#u!>a{WHuDldq;{8;=d=^oN^AZjae z@~~qdtL3Bf{;LLN6*Gm!oBi6US*wa|s#nur!JpURuvmfi%D42?r*<%;B3N({*<)xZ zI+Dh+Qd$vNk(snsY{BZ}bz7AHkJ+=N-~O zJp)K20#5KTDZD9b{m3q%x{vEtnUYb*qURnrwmH*QD8*^=sDRvqPmz<%Dk^ktU|@iQ zlZu%b^qBhh|#xq`W1maB8*zXc-B`QgZxg*A-iRA6U4SI2zg}wYuX(X&6au~Acm>!H#lr4%OcQp!(nixCiXBV==dPs|#U4YW4+$yf@7`iJd zA?U(MUv94t40lN*-9aQA`~q>)>b^bO6m$9ur3Z(0Ut)1*ble<7Z$UH*{!+X}=gk0) zTBG;yJY(1>{mV3 zF>)r1tXG>|i<@g0tWfesBt07qM@X@gdq@}k+D@!%eu2D<@myx>lFhgGV``mw4vk05 zt~vks1rnsunO-S@dEDQ^U867o`3HW-KC@M!lZUR(lg#aCDoS~i!IJ`6ip=AQn|XL3 z>b{l!);6d0`hi4WtM^2c(41U6ge&O93Ymwm%KvfD6HWb=8FCL&qcBp=Q zK>UiR`wF=|n_74s&6i-r4N=QiY&*+vUyndXG@4uGAaQ(Q)&z!#dX$XEb4;_K(|kyF zxR4^C+3GU}TCoCfAecBMVS7(elqbX?Vu{9@Pm5OE;%*4pjz;!|>NCAIgH((!Za1(e z4U?nPi(MTN*)l(WUuL?5#hU;@Qxb5DZb|bWT3pVPma|j~*SVJ#W-A$WJOeAtyFb{c znwD5bz2hOP6YU42{|xySQL;nQS)?7?Mq6eHC4XSsVKsmnGx(rcnV*A}aK6^|``0*w zEU$W1-zb>i@MP9oj9;cK+9PsM?=y*sZOO%8)kt-G3QKjfQ4_OK;}xxA#*g$4p0<30 zpPE~-ubHtl$jW?^scym*ls2&XNVur;PhQPp`_QjL{%3P9ZNnxiAk|8{d(RTf?v96FxPQ`he&pM8iMd8`SE5iT3YMSnl0M*9pw;A6Kn8@Lt7l*GTp^8V}uVLOqYR?;y@ICXAYBg3J8IF{_)_Tg1MQ2q5sgF{;VWA)HTqHdo3DEOoZvw7sCg0L4 zANhM#7Uk$R4PL#xUiM<{j<|T#$2?49-#K~S_F02!iTDKySDZTRD;19DXC7)$rPYodj$Eb>pssJV4v^-!+1NtWLU*d1I~scmJy>a~`z4|F42<+cup;WfnX-EDOz} z?rR&J^qjnlxzkqZ7PB!wqRYrQro`L^Ug-H5T_%6>vBTkYv6Ii=4OuP0M{~_5z-Jf_ zhGJ`u=5kLgA$V-e*S1e?jmMvl6?rFqCauRsByKXVh5wc~TPC#gX7Q-f*|bC^B9 zudyZZ|J1L?jEmXeU5UaV-E{>~-{8Jmp)#r9+t>O`@xSAHMf1|M;K+bAXME+JEK)4~ zw1R2=7Uh#`ON@!ECFAtyLKu8!=k~^uqN93_H{E+j>yy{l*{%H%4?Rcv#n~ofSuD-|V0S?oLPj{*|xm9!%d0 zD~bW4Hp~zeF*9@Mc<4BDXnS9$$pgiy=)qa7)qvUxn&}zZN*4s?y=Gm+dl*ZfZmh6H zgpI0abw3tUte>nkZZcPn&&L2_T1#L}d(SAN);yzG4TrL$aC3DJa6~cE6K|(cej^Hc zP{<}~++8Z%fD*>UmYYxkn%NU6!pmQuj9aB_O)qV&KT_07tR-7+`aUOz&uimhgu$v% zjK?xtj$37AwYOrN<%j)n?6%Bx(+F;SnubOgYRLUBqEU=YsrR2U?dkgKN5P9b*tl<0 zh=-(kitd_muYPM^ot46pTX#iiz?8>UkSRZmpyNjIowuBSm#$S)GvtvKnjCsK*fKZd zx@)@r-tFl|$AXA>*PZ~+QYHci4IA_@tW_*(nT&Kylt*!gxo<~pN`w+M{VFvshRPBH zC-61}Um|x$46s7c9G*NV4gtNqj2iWaftB&{y94?&GEKiV#4gc>G_)ROq`+ji!!!eW zrr9g()y;@GGG*H427Ks`#g3ex6!>iU!m|2R2kaHa_nTd0J%96b$?|@fuIxOcV_^qt zVb55`-av}&iOEKt-lRCN??<{J?9XRj}TK4Fk5|Nv1KOyLZ^O)VID^X(h4-0}EFV9RM}~ zDwfeN{yvxM1m}l1tL=0w@7Ti~A6Pru#?^I_0-om*5EUnb3F{CRHHj6F+{ZA_T|}sB zaBfo>ona--hUUfgi*uT*?TSSjq9#y+G2c(xSz@joKV*mkVT8%lqu^3;c@p_C0UHTc2>H`Lv-2 z%Z1M5h46;ECZxWS;h3FzrnE=j4>-9?Hs%eul?IF^16l&5i}?nsFbKAttu8Z9nN%mllF^oUu?- zGTJgoVr499#j1Sq7PE?$$A#B)Eo?-x)5^Ap`784=0~6PE{^aza ziub+XK9wAjWSLEt>G#SJXw~LXrGec(Z_M7_@T(QQ^a-Jt|LCi_u*Ddrgb=@A)#b0^ zPpLe=O>vfM>DBAESHGLpl8QU`ojYG3G34TcP2K{V11yYO+|V2$`&;NcH5)0HA|fqO zD#yQn<%A{y#WrF&90oMw>)q7>_swBl-RlqgwQ4$+AUq6UDIvE{m3BHG1^o(mDRC4< zDQ81(Y9RMU>)oyt`+*8s_|j~%5sPFIQqqfqhBjXd(>Y1QHhLvXe4fx_7`ne#xhwc}m*I9=)W0&)6yq46_eJ7Zm#?r+H|*Kok1sOO=2(+Vo|<{AE&bAti+vxK9p7!xgo%_?U{ zS5ShVgmzu1zO~U7?ScWtDr9$9MP4jQn~U;Oo1JKG+}5^q?+*z?CHR!%6JWB~RTVtB`cqZ1 zRA;{IpHex+BlF>y9BTh!D*J324BZ4RcIzuLdK%y`oW6V-yKAZfoJ_7EIVbdbbIp$yCJh71^D%TL z&E+h;7ocGcg`C5iwhM7fobC#o2}p)P6lF8|Tlp3n_b?LOK_Nko$BxMfS?}qKj1xzK zknW%choIs@6J0_#qyAZx$t!$Hl-v)^>uWfIBY9>+F9ahlbYIS*yCi&o8CHN$BHZoz zHWOW7+RrjwO5xF|r>`uAr)CDz3cc|0=UZ-tIXZsce9<>W;F)^b{B>|}aKKl_6k0zU z&%Vm@w`#ngy68qkzOZI^+!=?o>bZu`y`@9R32)W!^s$_>SA%EW7XbIwr1x#f1`O+1 zuJS$cPYtxqQV{VhRe7j`DmW)Og?8}9NPIe$0_HzZ80KGyIAtV~27ug@f%S@FZ((3D z$wYVMsMm=#9tDnvO9c9q0E*ARmD?``7K3^Am+2*7xozM&^9gPIWQ&@$h*@aEhBU8U zBI&XC%~Gjd#`G8S`j0sOsf3g%8JmkbyAGr`};WH(7Z4sQb7e1dbBRtg~kgbmgwz!N%e~0AXs>favf%_Qh)i*== z)`v4PVh5x}Z*RK8r?X%~WV_QTD9^B96sGQC@z`fWr#BI-a;?cA`&D4UhoWjv=ZWXp z7);kh@7AHNN+e9}J&2&;5fW7m-4f`LKQx_0?J}}I+K_uM&Y`%*| z8i2rWP`_K8s-ZPzo;>i~X+nF^Bfic&i#{a{^zDZAwfZY(JD$#&DW&f;%*?d<&3v9Et<7|o zIKqAcOC1(FCR{RIJtnnuy>`QB7Ej#7z!V zqXO)xYXQRa010s-Y9b%n`+3?RNyksmQ^cmi3jCoZr-?h;wR5i$n8jL->0{3^cF6xV zN>{G0h!8I?@d@4d5q-3`LQ~!73ktQ$wa&EmfX6f>%z zNkq^TSO*xh5Fw2u->FUc{(HR`)!muPteV3wq4B?^75&~~p8)g4asCaW>sJ}pMSczA z8h5piy;-`(iNb?CN!P`C;u=O@VW1OpzIg^d_qG}+P5urw1C~Zn5UdT`ejrDa(5llu zN#1MBKL!J;pEgnOsfDa!MGAl2pgz*DsiWUmjkdhG66UtTDK%Gee{HN;qF=WyWz1gB zs-ue>$>_1K+D-`vS@$4LO)ckjEpq8MO?o9cf_3GPHACBp@E$?9M4avI?UP?1;`-Bj zPA`A@`xM8mjE3XhUi7WnjL4k~%1>dfPCTj=09gDx&Vb`xs`)Jauk5{ohExCmN zf8}emM5*r=wr8d@pLg+#WN*v#A{RFa^1wK)=|NNA<(i4BUz7UKN~%>_i=oL$mO%BX zlskep1TPv0@Ls9^w3yC{&itT}@V=1kyhZNKP^%@ud>w!Y<*VJr5PEiwaPlPF&)f>t-#WMBcrzdW0g!Io(raz7HWy;X+1m?u@}CtzMsY z6AzCCxvQHUost(IyXOB8n<5T{5KBUS8{gOJ`rWrpGcpV-k%3{6sz*P=im6RuL9y`Z zde`zGZYxRHJsX=vjlizZdrWozZ(4P7K{X9>4)&(&VzRx-?{{M2U;~Qp?KLe2b~-Jy zAhHRHIWur0YdmYO(H1$8Fa|f*?A&^Cj)he}DUWs-q?VbB>;IQlG3Zy|g- zF+)$sUQWZa1V&^YTzm%Z@HCGO+FY}(HeVA~GV3Kt-rdAh?Iw8lKDwX86n_|FuVHVV zI6K`!y*jEjqn4*vdg3oOUSMNyhOc;ILbCQt9OZi$7LiVU0#$vIkPqh8vmZ z4FP!)da{U`rkKS~Y46$e!ROV~ecDwju~$wnnVcF>T4B(7oq&|DECJ3eWaaU>ZHpI^)u^$lCO+%C9xizBJrw`VnK3Mu?)PHE@9|5{zPs?>7^dA`tuIfG z3mhCbj8MeGi+ffb6>l}7-@P!;)kZ>mSG-kK)m7PChyp#xHX>_$Il;F0b#49N@h5B4 z)+0rEF5L?CDaQ^U{Qb))w*;p8qwmS#Q!KNabt{1)>>>2$_q&ggkCFT(vNIa6j4oR7 z)1jB|DnR<-gam}Ly1&B#XLq96yBHCUS69R>BC%>Z-VqOMz893ZDgE z+mZiV)z5$VO`Z5|BbUZ_U|%CymC%|kY@1h4+hYiU$EL)BS^fIC>xfh6psS}Wm2&N& z5TR%z3}nT|v+$hnr7TUduHP`*ph+mpuF3p1Bj3pZPb-;fphfxIV*!hzk+fzjb+vT* zoU_f%m9f1iXGnn#A2y-hFgIlE-pjq#&4SP6zDHcDu6~v84o&iPmGJgqZ#nwQe;lO7 zE$r15=zf|S*kgEYY7z_sG6>kW69gmH6dPE632v2MWS>TR6)pa?Gml9gcLK`sfjg~3 za1A}yYy|xX5fxWj*(I`FlL9wHV=tj+ZocfO*V6TZW?j610vjt8Z9<+R*JS;zrJC*z zxxtdxDEr4&*G}qB&!vJA$|0M#&C63>j|n;N4?qz%!esj(biSfTg=)R2hJHpzRbz9` z;5SUbv(BNuaOuN&jla+4-}M>VQY_Q-KhZGLONO0*5e3={$@2G%`QpMdlFR@VF&2FM z9%NiXbh(rG5%LdGk{1`EXo8yFbUlmfsw_ze9j>vmRr>3Dqi)D>=?m@Xod{wEbqa1GE1mSOHff!x^(=C|mJjizZi z{?-fbY2_owr$qJ!4y84$%q_A`fxr8$BRatz?Znq0nrFwBdG(e3SPQ>=)q9aj$+Z$Z zs4m@HILg(sNIL@hwT}cIi1Nbs2X?4NBzkwLTlTZ2+?BGxSyrP2g;t>p3vdG@C$POs zyR(1WvruZW&}0DGKr6X6y|P#O86BCIy0Py2v(U7VF6cquQ~y92SN}d16%bdabWudu z@xNx9W&2KVmS!VgC8+Br_vI&C;vznr#4T!n$1nj^jFHU8y>_f@DRxdWZ$7+JalbNW zJ@R4JozPOHD1VcjGo?wx$6fjDDj^FScNORC{OaJkMM<-DsdXwFs<^$A~Z@8YoPsQjg^zw3K*|EI=fcyjhpUQ8`n3MwKRW43a1-zgUKmL1>kl zOhdTrOz!NSUSs;N`-HD+=sh4*a2ahkSCnD$k@5js1+R1vLbxkWed^+;Q` z)M}vMtmNyPK z>!;9;Z?gAGgIsl$gs=FB?5Y<0@xNy>CIi_T>F;v%34Z^uLJQgl)oisZe9l91Lh4>< zj6^2{Y`lou720>9HsbwRyqoO__Vw0Ui~d{W!J3|Hc}=pGgEMr^Ik=Oxbpq6g=xm@R zh=Et5Lo^T)2}6qG)Q~18MEMVR1fF?%0+@yg67>q_>1?#UpWn7=00ef^So(R7+~=&W zeEY{eu4uauK7+qNvg8J~8(+mRPH46dDF%?AP%lPNtG3mOr;bmx-)j zTv2x&SEG$DZw9e%I){p^=w3)y-eOqoO!lWP4(YK%6;cDmV>IYhN#^)t;**kle^(sF zGcs)Eiz2U{5buh;4i4SVp1!m#_7d|p*fEw-V@{jOejOn;fqQ10^^n4JIlt&~FKce) z{EV607eO!u;jQ{mFm(-!9**7+5}KbA1A(usamF)F2M^osQa)s0ES>0vt^MbGv1mWS z;sr-br5rAUT|JG2uIHkJp`+?a0aEBpHfnMI>i@;kd3dwE|9{*b7qNPcirBOlv08-K zik8xGTU2V3_S#Yep>`;>Yt-tXxJ3yi_6UL?rKj|thQ8o&R2sW zRsuY4ZJ*uDK*<*gy1N6u!idFS&gL1tU|Hd#cRPwl-vK?n4{ygZW{l``nJ#0LzxOm#nht;Z=T6S(SW_y5w>SSjw;_S4FQo(5kdj@#3p{1l5G zjBMY1@KY|JB~O8!U6dfY?FzW-(dixd{|j5eVLZ4_yRiya6V zI+amrvNn~fdX#K0H+TUsmss6d-CI4~wOg*sq$zKQeuX+vK4+Bv6x+(|y93jK-{a$< zqEUY16Fw)FW?#wo$CdsXg(~YPTrC`}$%eyH!>{jhl=LqG)#t(Y4sSoWfGdZbPWB2& zTpq^LGk(rxiUEssU3c2QBffq+@JitsKoY)i!tIcVq?Bcgxr<+GmFmgZjKC(tJ_!YJ zza{NF6>s+0W>uQHdH?NPK~eE77(8alc|#^OT^#+8qW-(*TFB5=23Gf@5QtCTnl*wpaYJub)IeQHDkjA9RJkq^?pUU_q*SFUqrPsaW8LTq6 zRV08Mp4CPF=^n#hYDooR>VAuc{j2@rUMGcKe0dulKVrAQpTDLIN{cnivUb8h|G4{% zyN(O5#>ywT1vQtSv(*jr?CG#S?`U4Keo1aB{j{}=GJRkmP2JXz?{&vwko!Jk)Pi{~ zw?QHrH-^-DHdpErJKk$LMNH6@4pYqUomuJSYW}dP`hHQ5ua(2B zd;BiLJkndRv@o^3AT^1|8pPC%>`Rc)Ez=b*dZCa$toybZ%;zD;=eL-<)elL-tak=t z_{G;^R{NzWd!0LJp{1caY)`hVUwHG&JF?bWSYgqKC9{o#Gqd4%0wE zV)IYIY!d?7fd~FA!}XK$EDP~utQ6Z|%RNXG4UHx5T1de)7gW>(@&Eq$fIY6Qwhxh! zu$~VBRK5rC?Gkftsqt?gnOVuDH1BEWl%M^KJl6h>Zw+IuV+@2F?`oJ%6mi8RoL=|8 zTdn$*4RG+G?8kP#Qb0i49%xiaBsj>?oOK5EFX4{=vR>fYQ-#s>^~UxJEgmPp)$=RG zq>E_NN>=vv3f7nDbb;TxD=d>sz8&0)$zDm|RCAuC)cK-Y)d4GErdN(9vz3<3D%i&+O9SM+Q-xmBbG6*`zYHEKQD>c zq_Uk?7S&}RG)e7*7|+$m9wb+`3$&S3>&XM=?yU<` zrY8LqPSnOu0yFAXx*IdEiVD?fb2q_LeR}D4=j_IzC^h_TcIQ%+LUvvn*e^9tXJ-^P z&A%c5*`3JkBNn53%HYL%ciA9uFCu8!jtlfg2;wldMN%-nl{cxYCe?=Bmsr12t;=l9 zzZGwEQ#)QB_NWV%8)`Rrq>T5sOJ6<2f;jC2h^^&;C>HN@TzA7upC7MxXWBfhYy#q_ zgibNQP%Vd--RDHU1#_02W*OUG-@T?uZEQ=w@SIrc33)6Vy;|4>7mFz_XKT2gLeCIL zbB#w5Z~BJT9oy%cgWC4GSBh1C1N_ZQqXRbawtesgaPTeGZ!kU2%e~oO>F@W73~zGY z%17Jy^=t~QGKuEvEUi>$T_M8D)qY7;9NtG0RpV0yPEKe!XN}~3Gz2%lrc+9o`()o| zBT5%^2*2Ret&r#bk@Fv2bLC0KDT0j|QNE zOPr_igq}iyt5U*7jOWunG6>4Fq8PP7qWZ*9r4|{dj5b0T^j_$EK2DrcdJ$9x%92V1 z{RsO!k>X6+oJsAnp7KxnFbkKz)zeGA+$WM-&4L7KD~LZ=8wKRsJ_1&sWxTb~w!H8A zkrt>rOSC%E*XvRu&y-$zvc-Qg%KM^-{X_K>$|E+!p z{MVYTg8Gqp6v-HZW%C1hmj{6PNRP=IA>!%Y*{=7gwl{{a#8@57qdL&>a>O`k{YiEg z$H;U>6%nUYn^5COx0sl0>lZN`_Zy`lZZXKl1m$eSqwECyR497GWzvJo(-U-(y(jJh zWiF1K9oP^h?&e?=)^g=4PQ5z&8B}Q1nUCmeM$N2z^=)k7iDNoz@vEez{`|8Wo-wJa z^pW*OMXU*sbTdC`2!@YEe}Y%93(?iD5JiM6>>kpz8!}Cj?((!yp9VXT?)3J$jY?e; z$LQ=tTyzWx6|nVk=wU@~@&i!@I<|+l6+~-o$#LLhb^q!jY1CTX5^j9G4y0(WvmrR6NKDS&NK&+C z)zaNoeO0r`Tek{a3d+(3`22kpU7DbZ6zB?CdhNqNbvBJt*6|vtZ@=Mi;DMwoIny%+{G20C5`ItUeqHx9pMjWPd|BxMaOUS`jG-U8dSS{$N#ygxZqF} zo3O3eb_xJRTu#zim?(ys6E83|-BzrVo=gyxi(07^WRe5!vX3RKdG9!UM6|10ufo3# z{bA3vmFhZt2xHQQa>u(VF?LHp$*3z`4m%X+F|JKINhZC${ou?=+Zsu6(VU%ovhwP!S0H^7*^df zcjfrCI`+EkeDVYAol352uAt0cg9X0=z{Mh2HN9GW2-3EBRJdpfDF`G|@~h68(ceIu z>cIN&P`>WtBt57vCm-!a>S;zw|GFJB3VW6;p_sSi!KTe53ES434Wu@33-qYw2IfuD z(d~QaV%m=?5wIFGKZs*^SplH84!$;#te**^S z#`_Gu!}wpNXlu-I+uo|xOh?jO$I1TE`Tee%@rcFUzD4O19c0MtTA7+_@(_>~46xGW zRtmSgN+&3Q!C;x;mLr|%?(B-xcf1ctN>qg^)dQRE8yklvREEup&{3d zS6^Okb{WOnBC*3Xr;7ln+FX1h<5kx=gIe zm%z*{yF)RWutdAIfy*k_uE^V^Y~;k%*SFEWNx zrWkL9E4*;e)L~e1`|xCJZozu%L)R_*&)510e<90;jNLu|R=7d;r3CZ(J=ck@!xjwK z$6E4e2^nr)I`bp=Tw|>I>`xWgw@~MV)*_$J;n3SDe22(`KK%x%qUe85FSrvmQ#dcg zZV%fB=>5BHIkXV(TYb~Ni{H%GwhJC1?irsV#CM}%IEG7#pi4gQ5$>ByJ2oLkcLT-; z|0XnwAJqp(u#-~ry>Oz^A0R98jbDAsm0g$oxJQkCq`WLfzYn@FLOw0cbZujzs3SK=O@`zTbrh@4UEN7_uYTBr=X#GLIx3;A(9hstn`84@zlJ zE>v|>WYYsNWiHloP~W*%Fa3cFGPFYW??Y(`Uo_5#f419!pMNmRRu@zelRwzyMvPsB z7XzP{5?YRQYZtSgPyI*7ZKX(__9>{Jc4-H~f=To8S<~6!Ow=VfSlxjkeufuwTXRit zIf>EhzIh~*<1^1j?*_)(cIMGvb`2L%FwnWpy5qED6~0|CBj{APe>3PuzMs0m z?Z1%-p*W`Hbx$yOu6$!>s4AIhKP3ExO5Rf*XQiS^+J)8YPg$8%SfV>=sM$lu@b{IsyA~w6ce2NlL>>P8IA%tn?*x!z?4_pxo#Bc~Wt)!Hnrt_J znDh3};5vlCvZ2+M!#4b7$1p-ck+u=oY`q4|t#pH$$oJeTc5Y_@8z*9=Hv3@)A+)G% z>uOQE?irDj<)|HnjZO?jM5l=8xDM(ovJh`#iQJO675hU*atfxSes^xvFn|(Ja+w(tmjt0>$uZ*77ID@wPrF<@{9 zN1-Dr8|7AiL)cd)JSW*B^BrBC2DurujRx)Llvgrx)7_pWDmZ{>4_If7elO*SMW(*C zOVAq%SSA8y`aQok`oqf;nwRR@U|}?iJ(k8&MR`{7CPp`}G%FP@XI7>B<(F-)(Kr_V z01wi=l%It8n2K?UK&IJWa?r)E=a&F2z5Y48Z69^Zbo8BNb@m=e3>I|~N>-x~a1 zZ?J$yr=y2{c1`}maCXlX|7hPFWAA@;5S)Bo4%&DZbRUrP`pWTDIHe`n`^4Sf$^VfR+{%OTi zSTcaBEgMA&GIw$-_G~2Ab}a@|9Q!k32{Z34v-UT52bVb4Ta3v=(_6A$U$U+fBoKZt zd?LhJ-Gn-St4bIHz($El7=;gs(m9^G-mb}*AXtOk8&aFX_XFOU-|9Ydg0E%(?zcn+ zjj*9zy0Ja;0Lg$9#g3VtUYNC591-g_?z!=@ zK|7n96=S=LQth{(OqZNIn~C4F`;M&`x2S2h(SW=<@CA1Jt$MUpxrhsy_uiS)D`wx) zJ^&pko#d;B+8ouFiS=~7em8(|HOQh1+WWKftb1BlQG^ly^a0xydXWKLn#ZJnXda2I zhYQ!J9uu4yc-cNRY+3P711y{}Kz&o19NEgkdxG^}vQ&!8)?^(XV_|#hE7nHGYHeYP>LgQqzV%Bt^TfOb76}zjio$3L9 z`t!Ef|L9t}mo_xlI)_>V*E-j77APo;v`uXm{B1v~P;>%WclaKMqj{u-jtMTS(0jhf zKMbzNyjc3pq_^s_3Hr!vM&5~Fqs#L3aa{fTIA@(}${B}i<{5r!VPh4nMP%-qe;%L1 z_*%mRX?a;Judx;4CP%-srS4pFEi`h$V)lpKjD>CKWZn-lUT*2oC)3hO8M&tJWG6p?nncGL}OXXr5PEnARX-FQ4RAQbW5z8@cmkm{4HQ zXW&P>>iLlxWo?}U#5X^$evF?rG7prB&Ehf5tI_57xcoSt486bA424TW!W@_aj(C>b z@Hoxnj+Ivm8lc6@V9t{evrR9;b+s96cGRRFz{jp~?$>C4t)=@?4#t=c4DLo6I`+rJ zgI#2_b49G}JwJV)u06h|(TeB0e5Fec)YxM*UOf38+}~pHQ|PeoQYClj=_H5}4bqpr zdzAI{XLXz}-$Hz|9RK{=RU7X;TWmfdp)Z&mTJ&*>0LkcSwuYDZcoVCdw3{CNya7r$HpU9?+{|3b_%ziBkk=mX znJHM6aqc_8HpGeU0(a?lr-O4&pl|CYZ>2>fe_Hd!tO&Ftt93^`e)S?=wlOYzx|xq0 zQ?F{SgVeO#i<|ju_vj?Eam?5&DR*7Er7f|y&GbgEpj##+=LmV_v3AgY`($1runl>7 z>ixJZtRnR8aqZ3_-{?j7-}tosd6|9c+(*!of9LOap$QD5ip+nU#t9uY94Sx|HCHa# zgRfFoStqh$ZiG(KH?!@eKkdy~s?e&j(OmQFM6#Ch_TXbT9}G9-RlYcpGAGwqgN=1X zT>iO(loN%y~-AyF>=mu9&3`(l#*EH>j3Y7JGl`Y>4 z;@z~(dYa@&IlquTuhRBWCGF@6KGLGtsQ+>stuq05VO05M_#~@Vg16%+kWjDGejte` z)d^Ch^*n38YV@k^$btzdwzK>BCCwbx>)O>BC$EUm=ciVT4DJn+CKBvFEe6+ znPl5T%MV-5RxMM(#TUI#GeqzC{_H#$ESM6QKq-xkcfo$M3j>L4ehY(ztFlMrBR?52ku|%DBs9ly z>Uy}GHP`WXz(c!jksW4kzZtkSo4H9ZEy8SxuV776a5GlYV!HJ~cXug=5 z2wcm1FM?50ePM?X^N6yQEPajqd?KxIIa0Jg2@R*(gdeXVz9EQN(*-q0DcXs1@&vI4 zz0e0@bFAMtdD4<)gf6QZZv#ypf>%-ieUkxP%?n7URhVh05*is>y8~D0K7eOG9!h$f zkJpVoEni`qYX=l^L#+}H=Jn9aZKvS}`)wYt?Or?36Ndfkq|cBwQQHNu3x{ilPTJgZ z)5Ihby>xyqP&B(*a;Y*Ki}RC7lSMCoJ=L?HBzHR9@ocVeFg2Xl(lO~Wewe$tu}7Kv zC0{%W;jEgHP$1Xtssi~DMq&cK4X-}9*tq&cC{LMry}w$G!AL`rRDJ+b0!#vQNT*O%DX-wfuRB5@PN&t) zR@J2*DpD`5&dwRsS~?BLA9%FFlvj2Dt7`>BtgN3!*wC~h(9z8SCaRyeSWj!E5+YgqH9xPUF zl4Zkf7<9I!@RjM>&L|g1n-fs{nMOsIg?bk^iuCB=AnWu_+{1L?S_RQ_m^PiNb#aT4?Z1?M558sLem8Qs{xCyT@u;8jEdh~&qo z#not}6ZbLrTRn`r%Cm0fp{#HX0PAqCR)T%I6~AsY^^zwT3n-`mDJTGYpqtIAG%ER4 z?dw|&S~KxqX;({f5*IaB`@4CPD3wEC*QUK{KWV}l>JtgOiq@r>gSpRE4soB1K|e^F zvv$o~=x+=@&GDomf?B+jLZM5HrM<4K=4VofZI*e3)i>B=WFu7Kd4!aBM{-v5zd;A^Lav(d5QT&{ zeU-;Ki3J}TQeRRurSmb$g&hBq@=M1`econLHgT|GVbM>}wUN>J+}yxS(DMmYp4+Nz zAX5FeS878-jkg_GW@AAHo#WY5<3A$vX+`s}IZf?GD;yIb@w;BYiOby<@|`}pd8Q?y z9rO2%OY!dB`=0;;poZ$14=VR^^!0U$J!<-Z+R;R~8d76S{sH^j2ya1cbzNmL6@`t4 zK4wQX-J6={MtJmIysMR1U8C0gsz-UXFNd6cf(GSbws2!7Kzw2hV3GZeT6SZ1hz=nK zm(m({xAVAek_oJg?IUN?|MTiTMrY3TmpH@|umY-$Sy{awoawHl;1cZ!C@##(n>pR} z+r5Q|qZP&4b)Zzi+&imkfi5a-T_ujl>wR{EC|}A% zHM-w=^kQIMz$zL&Y`)yKwM&NbinndJ&75cLY-V`d?u^kbX@aH=@nvq!jj1n4$5Vr~ zIdEUp)+;8=LiH^d{ar?$)D_hMA${c`pvq}Rqe~lw4L?7VmX>nj`rx+$+k5--RaQFh z=E(6$S`bS$<{t0$4(8_@YoBA^nY|%_#q^9M~ucmk>J_Qful)y&@ zf#nhvlM{AaXLc~2-^p$I$kg+|wR$2hoB;pne{TV61^cvVY^?XS*U!?TEUnkJ5GjDsjT^^9k3>)eE#XaZj{sfy&f~RABPeU-i4>I9O!aoCuq;) zq>mIlhgOhKQ~%t^GU^`n(jLPf|7?Eu-S%Z*Tm_pw`*fulQq|Bb#6CQKOI;Yo;^f9Qd3hUS7HYoa?dXrUMGtB zTYdZAPE$YaM*FQJixe9zHel`-vb!RB+GH2CI%W%m)-)Tb2V01royS=MX4j(iRTWKM z+BP}m=FiF|@pfe9J(K3@Nj~sw;}Xa-kPYd2Iy?#cj_9RpBdY;iY1u~ZyU*(fRkH{R zMm@Dk0!x1elytyDp^^|$=}?S6Z)n9m1FU{UU= z;`zMdKAzZFg#3`~izniet>rz;7b)ZxGil%cdRgYo?CtfneK_d_z&7{#>Bn zf|vMJb(w|tuE`RI-{@Ah7h^3g^>pOU?J_-T^Kc@NNsh5?B?1v#tr@5`yLZ0lA>IN& zr-kc)pI>WpU4A{L9R)JdS-Vj{c0xPjJ0MNj9lzV;%CIRQ-i4J*QQAAr9H@yd>2CF3 zApt>#)8TIUR)zT39wq+t#p)gUE%q%0cQNr44)!9wq9s#}o-0= zbIAKb{dT;VG1A1eDZjf`qz0tHCSO)$sAi0P?rq}f+NeuN{%boLM*Cavtrz%Tts7SBrC-0qGdTb>$RJv>3BDBy0z{cs62&uz~+ z*u}sUBTaVV<~~jM?PEpJ3FChFt7}CXd&)G_f(sFpoKbkGBhtckj1~tp#=lyUB`v1S zlz=^hns+>W$Lj`Kau&WS{o9F{*^mwcWz`mD2oFoU&#{Po=D;9d^j|u**3>o{Qy$K| z0lAN!brOEl1iZj$Pa~hVS+uJ&%yP3fh^d6Yzp{XaM{oEX(d^=}Co`Au@CJg%%Pna4 z3d$M>ZAwXkk1jF=Nq}IY!5}lG-f;dkJ^AIL>~z%98i+gV)Te22_ebM12T_Vap{hpF zFV-u?ypTS|xz{(UCw#s#lb-?Ai?(XR;NtHqLJn4<5YKi#*JT05N`iNUXg|mxxAbQS}$sZO>+v zyX6RrCoOi$m_6QKv*~9y>d1#k7NfmY`)@~t4Rmfbk+-|ny!n-B6fk()PeaLWxy^2u zf(onhs3RvTS@b4~D0C>KuS^PsMl{Y0Pfbn8Ul$nKYU~Qtslh+I2SVh>7#nkZTv*_72QH*JF;nYB9~6%}N40gBKbS zthu66c!A?x`SWgZck@I;mZ3Va?#sAAafxt+#&lhFhPBEDq2bZnYNI0r;ZU>YKFd2q z=R;Nc5zq*CoyRC%m=pTk-w3on>2L)Ot+#iFwlK%dWcPTXQ@Hvq;+zQGwYBT@)|^Pt zCC7hsibf}Gb}MbCzbkNgCIaU^QB-)(c56~iVgl^*3`aCS~cxSYzdD_bXYlALRv0te=-6~ zPJUm2)qeErIYtKiz;HQXtX!9iW{)#i=MXbicdAW&ox;?bP*~4_g*EKR$xzKkudnm| zMrn;pO}Pu#`=ugKcH{gz2PW0WgpM=IGl8Krf$1Xsd-mW>tV>@}=8;~NgN1^elhc-O z$DT%0YGQdnLkG2S!bd|caIXHUrrmB2iyGO+eoxN|Yu3@@#9$5{w@(;dKpFdjFE*#jisRn}nNH5LY`!>QuhZ1xsL_LZ?Q#1sTEc?pqkgk$n189_{V= zH@ZKnC8y*VuiJ~~DhYmd;5Z?YO6)Ou8!NjF`8+U|Iq>U&<>`h)FP!U zU^YQlJvg06m(9C#iI~( zDF{+P3@qHgAZVUso@8C}?_Yc*Rvt{9O`lCq1;+r*3_^L6AO*2WWbC}4lq8f@%^@G( z_hQGbO$*)mrrvLf22z(t=WiOn9XMaTUX5onpuM2&Cw zHd^McoV`G7$bJ8y!~&{Xe!z^BZ1=c@2Muk9tJ;ynuoK|UlZ1!ty8<5e zt`t7WE|T3#t4Bo+Lap^ILBU_G&({vS>_=!sAFwXTx_b@G*O9RYaDdKF zsUbN82f%iPeYBSJN+n-c4Ldfvx#u*iFvSmM4m}L*)PLc7k|3M3AWM#9)H0c907DP+ zyGajU0-(re3>#`)gN=DXjfepAsI6DokCys$LD1+ptqS2zPeWv2$_IC|31P>~C{Is% zI9$~xXV)L1xV5N>xf^Y}vYd%cLr-SFnm5i9@cFeH;JC*Z(b9 zS^HJZoHvGMTfN(==LJ7cZSEbHuMSx*|3pFns z)bFOSl+3C>isBKr{7lEZ5I(qSH59v1L1k_+L#u!}xVjt`xYr(w+Ie`$`;5|1UZsXR!DW>_6<63jh?Dccc*f3wTIeme<$$(LLxxSKMvA&iR9<8-j zTpP|UI_c+&jRb)0Q@PEA$8-}wg6SGB&V~4{brZp z_Mltd=uDsoc11C$XG#FIJ(oWJA+!sr(NDvGy@d1~lB4H(gBET~$YHb8A9tO$MW%Y4 z))W<-%)gGJEHu8|g89nzcb_^VWA5__9)!)UzwR3Rvk#G`am_5wR^*$8)R;*`ou@{U zq?25!`x3*UIs-fGvZVi1kiw?Zmi8AsU%m5~`5Z(>_gzG>UEQ@)$NjjVhH~i=q;B3W724g1YVCjosPPJ?viZ)7+kD_7n!~13WLCq{-Lx-r*O^nQG zM#8`5X2dJmNn=n>I_YClYNl14J1n66yP9hqngUOmIy;H46J|q}raKm2zA`V=(Tq`j z>Zvkj#W1qD$jTAnxDYQi@X|*zNJ0A2vy@)yA6*3=1hKNbz<((n%5VAsoASNIu9gCi zgDH-0qKx*u$c^M3m&G^F6x?RvZsq>1-`wQNLpgp>s_%(WXb3-ZbMyOkC;$JcqqN!4 z&NrUbgSZu$g#KI8<2J_gp?{=U8N0aTy0jdE>Gb*BQe}&S-D-OV{M9L9<|bp5Ee3_C zi4a=j1uT)j8>z^#M6v!fUoIVC64JL&9|louXHl}CgnV^UwW9@ZU!d1LvWxFT3c1nJ zZEBPjE9F)1|LBYlYVzGyht7*#Kcw}(4zV9U)C=C+{gb3qYbl|{ zTBax0vUkkBMdp5I31)dTw~EG{O0>5qt+Aie-a$#)$}=_Bgshz+&jw3&R>(Uc5-T;y zMe--7Q&i^U#p)14Ail$me4n;Z8H2*xKzcU^VRs^R5i8h2uj7!}7C{hzBR$xv{78qa zeV7nG^f!a6XMZH*qVlN(UiM#>$@X8cHSRgVJ?%fwb>6TQZt--_zZ{K?SVqW=8PuA1 z`@L}M4>ZrZ=j!&|_Hp=6Z|{2)lKex=I?q3lcg9&j25o2C{ci%S?&;;5;v1>?xvc!k zL;A}4Pz62B-Wpu3Tp~hk95`Fj@5{Fc8Q73VDb$8k{_24Qa*s@JKu_V*1G6n^DIepG z_D@!1?-F_YbF^ZDdfwX(g0fJSr%Clu=Fx=FzE<9F0~c~4YqPtGD=^~B)-X#&01?%{FQuZN;?fJzVq3LWR3E;2w9` zcug**AKdW2>o@e>?O%dD)Q!tR;g0If_c$!`sv?4Scg4rXfM_0sFE;*T=n3)99M2mF zI&pnTf?R_7{eZ|9>c>&G7oHaE{-R0wC)KrD(XRZvZ=-c87~lRJ!&3YSl$!?>oubN2 zUU(Yn>cKqA3Afts`C$Fx#4`96(|LR=%QFVa|e94|!wpJ=Gwa0ao`#R%@TPJJcxRH6Ytw zGDkt<@3LE&mvpS_u$)_LvZ}&b=Jt-jU*Ju z(^MVViNgAeyRmYtN&1Wsf~2HIQTi$^aLkxP{QfWfIcz|z8yw#A9U4{vQb2H^HEtsE zJI`d0a!>kzq6OsihyK9!U|!$Q>VqS$&UT!~d)veMR1#fxD*M` zYHhqa@p$wwapW|cxx}}sK~3@Dns-C+LLz%lcj*{5Q=Q-g;`aOXkA(xzTCXijm?>^? zH_&?yRLJKp*R0TGo|#~T@rnt33`h+$|EeD|_qjRc#V*&X1t#ArQFfvk@Km(RbNxd6 zpFKI0XY$1K7*++xQ0L(#8#G%{4YQ&xmp5u@yx zy#`#Jp)og24S5S!rByALy8t2S8?pwjvH7Op3RRq37AGWP0#cn??U8N(WpoxF>Z2x3 zu>=SN@Q`Ss^)MQO2jw%0MCsk@gQL{a3VEJa2H~=K*}o^|-q2&OW#Tq=78c=cnc!S? z~-9ncHdSpa!Y-#Po&PiVM>L51U*WslldQTX-78 zQowA_G!LG;5Z-;_xE8V8}6CQ|d%LG^mY6 z5BBqpUQm-Wt$sRZ|KJ>ba<#H9@Q+V5>Yl3LYXk9Q!JDUIE;Vv*upf68e{UBY)W7?^ zn{qXXt_kx`VP9v-xWs)2LTN7IC%>yx_97hI49A%1w|ObQd5TQwJT9l{x4sa)ot}SI z>M+za?bB9@&WWt4jo>4QzU_bq!oae)-~At>WX&R@WY2kJ-!`K(09B)5s^_byKAIlD6-K8dff1 zIf8#paRZ*d1_jT7!_)55t%Fr}p`c5pPI$xZ45+sipSepn5=#}47x~ih za#ysSd2pbXac{Q|&uCXW@a^xAY6(Y?z0I`hvX!oq=0Tm#CHs^pO=3EW>)kh}Z#223 z>gl$2uI>)QzXi#hAR$obYr9WAgoPhyr=E@h&%x^>{G&r@=NyKAFH!mNTZ~sxFdjXj z@x+-0wj=)@(FmhAy0zLEOQ(hYtbWaanrWdFh^GeDG*dh@%-IAvOf&zZC$*C+SIFxa|AOr%6S&z{t8uHXqgCl>Lsz)NF5%nO%NL=m(@1|0&Zqp}c_OHs?N=dd7ePKY)CA9Aw)^5YyRT57f9dJ&&Dj8{%m>5~8{0cNeCwwrtYTZ_%pS z@Crfs;?w15^&x-+&ST@=+7Iggrd*f*F?pApS&u6OerQ>sQ+*ZLSWUH4o=Z zyZqni?yRp$y^IF+xLkeEk9Ml8XRdixnGzO9q8n2OKQyX~AuBy0Qs1vHS(mmv5efMa5hD5j;7V@X(6z}y}RuUMwyV9Rw%bDZwxy=oIVR7XvN=oupl|$ zckS!rjUNRc%ST~|AUok0-`b-_Z~KKXw*&4J=Pk`vaN$3P&)rBzLv^sZoZ6uchF}27 z9H%|-q@3fcaaL%S@p#$@A6P_MsylKh?LM9@=`u03%h`E>Zwfg7Vsah%JC|pXOLGW| z{uO=6aDz^b^dr{;Tp#2#o6bJo(*}Y>=Q^cOX8&-rWwyCWPyKN}8iG84XzItm>PtAQ|$Xs{?UDf#`(b^eV(;4!4PwK-;EnV~Rk~pDG3VIN@LJZa`3+?{VtEB#qlChyy zJ{)wxSl$>i4LvhXLo5M?&6fNETq*u!{J%x-idl2?}m$<97Ovr;(Sw0Nx%vCN~U{|qD0Uf ztF^;tRK`;7Fv=9=Zg`OPU?q^=`7UULGzLVt!6k8CwegPFNRt1!>u~O`5Bg=&esulx z{XztFR~A+GMJ;|BLEBGWLC}0vzbY-!aIRj{CaizSON4!s)GTS=xa0@+UxGeE3hK(Q z2e$ex)+Zw&?pl;0qY$dV&~$2?tK~g&Fm=#LozKK9+1905m=T$l-)X*B(`qAXDr?Z& zy;BsA(0RV8?zFr+#a`h$6d@XEG|_I_6gRDH0~{=nE0?x5+m3O}b^Q>)BB_4OL_m*7ggHJv-eze|0kB_vdY&I27w=d768brF5ZwY;lgOX& zd5SWc>+qg2nDeo$vha=fZwdfHk+v&-xIuFtz4yWLP+0v$KaZqyPojXrv;Xb3UL_c+ zR~yJB!7iQPJh5`y#cgOB^6<~1vU;nZW=$BarLi@%GG^#BX@6^vrBva9SKYO;NrR0L z+$i-`w$WOQ;b&$NF4wrq72w%5e2Z{DwtA45P^eTnV(0Aiy%i^$C+&KYH|NkYx9?#0 zpt|zJ@T_?4ZJIxakRkPZWLssPE+ou(K+?xQ<_>{`@(8DK+t^UR@w26t~U7$9! zi!G?s0~}oQp+**{r=hHYts&KHKqX(m-j6VDb{zkY5`fm7!&k^F9?^YQ=414~$Gkr|K5Z4KCIe05sVB|Ipltrg1v@W$AZa3~vVG_y$6C9! zrr@i5qZidN45f9~{lJ&z;YP`G+o^0?9;)<>-7{BWQ&~o~XV}AuM&I_`>btw$ z08squ$!YGpEObN0=*}OvU1`D7o+hR0URPOaL47>0Lj1>RiEatvNEBO>!J*IQ6zEpd zCA&_sMnWS=>ryoPy56qKBshk$u8{c_#w4;BivY-w>v4q%8M4)U9BXDSOZ`~xo*OP`D zSuwtfy#}$H)pmK!pBAB?$hwTQO0%BY#>JimpaGm}dZ!QLMV`gKxCAdtcZ|$JxJ|K%e`hVp z>6b^=Pf4f$?Hfe{269M~U#hM)?sDHm|Df;BMKLGSh;>=PwE1-!6>>kzR;|;+D}7)p zYfn8-Vu6^aBj(465_$BJeQSss5%_8|#Q2jf$hh`pP6N_(4qBB(%-Pl^MR08bVQt4T zA)!+~OlH+m8DkYSBBS~VY<$w{6O|1f*R3jy=wk864iyuT|cTxc%R8>T98(fbpM3sFkw)8{ibpR z>%X49tVus;|82MJMs-1B7bp}DPuK^ha9h!_YE&!(e1~6Wsz7Z#L2{t18%2Mw`)bl! z-Y^gJ1D*PZVw5-g$NZ%S2;;qaHV60kKHmXp9;S*~Rr9X-ZHiUgf8;R$xu*nVd;QnM z^pT%usV396(x#(1hku=TW_NX7S35L9UmexlG8=GRO0jlOYTx2ub^flqyn40wADxld zBVQZ6Z;4TwR(#zja`&!Bl>|`EnY*#Pkm-PdK5&K_$k$4a(LKC z@4PumwYn|s?W1fz&GJGfwp%X7um7f1!sdihV$L~V&3d(R?vkmsy6=O*?L1;-)P+r|J!)~_oXVkDb^;AKL86m zjWr}?V?hh8moJshGQ?i-b~%_VuZ)XCmy|(S1izAe>Tp5XMoZZl!uddARvVg(8Ra{AXbrH8+xV5RzgnJF+i!Zg`^* zsgNfh{OH-4 z&~1<#)v$WWbc7WIZzcqwP-yr38DGEjT#2W>ycSde?Zn}iA!a>gk)LM{-Opym7z0nC zRepAfKXov%^H*^c|ClWQli$KkZIVl7c*glSc z7-U2J&Mzx`GMDUWU1DjjtwBm^E2< zRFT*314{QUsdY)*c!8o$xml9Uno;EGWFGY)yuuf zY{pGNNYNIgdin#lG(GNbH;P~F@P3wkn6ksvq9b79#`7fT)iJmIVeYiClf?w!*;&1r z0?~t_+K^Dq?=-|ycAS~ZIuna_eHtJ6(mF*3l5#o8A)TJkJ`>L^0qdnMmGDW2-7!1* zt1u^P_ev``iHp?)R z`$X1)xC@`XZ;XaY*RS5*Ca@DcsjpQc=1US>(|$3Tp&4wm2XxwJI2iAUC9$C$6H|p z!&U>{KULPsuCF1#h?=265+7&v-djnaH00$@WslQ`8!k6_w^Za4*nRD1hmMq48psX8PG`T?%;b#`eU-uss2< zm~LNMv3nP6s~rt6NHWKzEZ=5JFK=WQ4iExx#dw|_o84}3v6~YA3MTVSIwm%(NFDEQ z`#SP82lxer9dYLe>t1a9+2BVLt0{zv;ZVnJ8UQwdhB}l~Pmhm}Np|DTPi0Ww(+wZi z-!*FRN~xe@BCyu zaqQfTjsIUIpOOAd*F>zJy688TU+<4)WS=f@iwHwsSwuM|D9mml#N2Gp1I)yINn=+m zuD?qqmWmQ}{>}6`hn`bTgQ zzD_KJ04QuZ{%X<+0f}{tj<)0KR%c&x?#VVup6NmWY_W3P+>ir0+)n5}jz&37$JmQB zohT&dhRd#$WO8z0$8U3Fc-h%9EkfkO(Q)c^H%SZ|8!pPf+p!QSIw_CID3AXe?U%1C zxo^p+;S@4*W*G43uyDF(&1t>?#MF+Ptj-j#U&F=zBwTsB*2k@&@OG8wAGX)-VIFnB z-%XU^txxy6dV!!a2c_#~zweH|;80Uc3-6yIrjlP+uXN1Ib5yD2lc05rsgE`*_dGO{ zezo{#gSe9xU7h}2cBTXyN30l>ddOIxM1!p}v47dY)szd~Rc>xJ+OUcd)I=`ND@F=; zwI9hxbpU(~*$(W-&;p%2Gg2ALIw1kgcaYx?wr?NChAU?G5AG>pJ|tEm?6WTVwI#j{ z$}C#1sG3^+LVf&p`k(_A4S6LRmYp@2S9wJ(Q9+%Q6_VWNAYJF(KuXWpO^tqyOR1j7XiCfO!`N_3=h7#K1wet6(? zf6=6Yi#PrFJ;z+U9?s{G{a)g{)VqPXIDTgT23SUo+@4j&y8)vDp~MCcE}9L}zj_Fi zvDIlLSuRjz=&pNGo2j%%tUHz6JPu_gB}bjx#Acx^Wn-M&ObhVQEbcN$?P+Xo14n$@ z)#c3~+P>$@Ni)t)U0J01b8jIN(!3vGSncipTG*=9Auu?i)5w7YGICr>mHD^76;#-Q zPlhzPbHCyU03k9Xci6LOplfUpmr%Vv>fhZ{7Sq5&=ZHN?yW#x=XepCiwDy>td|M{V zydA@6-1cHp#u7k%Cy&84pGvpH^ham^fS>tuS8jk`+XmJwyMU_c*%D;vL4WyKS=-X= z=={j;7RlY>+^eS(kSK6MNcduLmA34`x$U(FitUSL?K)-jUYS5@4LL1+1c!Pm@z!Q# z@)ECsAfm8q?j9G)#N$SF5U=rPb9)@zQaN!<*&k&Yn%t+nwat&Uez(8!=+7;#h<1NW zU~O|;HdJ`#^A7u}C&W~erx*84NB`5khjtIZCTH^d?>2)xq3H$b#kI5qYWU%+$F)|y zMOJD5WZuV)J%5}|a`Z7f#oeo!?ZDnb%-fN1N=zz4y8w-1nC4Af6f0T%uWZ}&6v1*2ysyd-Uc&1is&FZ_3{$!;&hm)!gF zMD|b9Hp($(>4FNAPro}9b|%AWheLVv0$F0d%Ww|0M)PfDX-E#e6OOR$w3S=4tliEw z{+4||C-Y#eV$i;<@6#3A<6o{9+Z{Bm5YRaa0Y^x~QS;g^4PP>}RvDoHuIV|2HUzkM zdnAx}ng2_iLQ@ihT~2ms2hFtcKi`H<2894i(x?R)oTPLl-@;g5!Se>5|M}G(Q=E;| zt#{bd@L#ZM>@WScTfDO@hMbX-``oy4-%fo;Q1^GEN=)#z0+LFegY>*;#Em)Li_W%!64~in zceWEuV>20+5**nmkMXzCwGd64qwiL}`V_8lH*8A2>>-S5q3^px#)C9<15Xs)34Rhkr|1DzHT2uu&@k@4|h@v-5oGTq{SS=-2v>yMSo~j=Il!wZ+hV z5{?)1>hubDWY&~`1l%LAjyo%Qsjbkgr>bTPal>1Z2`TM9uNU&%HfxUk_(v~b)xv9- zHSG!b{{Eb4{`;#(i1~}O<;W?7^>;7z&{+P8afRjTL7jQXWiVCK2(!+Ar^$ql?gX*K zy8PLo`Xt}8CQvXwFMJI4i*~I2k^{UI*G6;Gh*txOax~K8p;Uioj&1O>uZlJt$$#&u z@zr|^z3R4h6fxv-)pl+%I}74j?W*^HLNuf1jUUp5ZE^0gWwmhC*q(Aj)0Us-JT5)L z5#HqgvPHxWIC5K|U%xg}OV9^9tp?s6zdIa%lp4-UZ=wJg(dwMM8V=6odg{&L3F&P9GgW3U^4 zf|v91-nm^_jSq=wu9Y*DYB8O!X58Hw3BU_Hw!i%{_mjlZZ;qs=*nV*;wwjll2^k6S zd58DxhI4WJ;qEHrWhP3857kX0SaRhYhg}_PdEcw#ilqG$Fh7`_4mZCC+hpEhSpe>oA4EpKC!WUP{XaR&+V%A+@g%4%*j z0@xhHR_v}1R8RlqRkZ6i_T?j~OSk=zq0UAF48ebUO`2PE)@}q;mlut^;RQWY7Q8~y z5)v<+XZ_IWmw{eb7Fw+hsPzr}b|Dm?Tx>!-ncDW|LfOrjci}w6nGNCeaFFHa0Q?@U zT?hlT`1VoGXc6_TjFX_l8#c6tj0@q%mL!ZB$a!LUTHOmk;l>~bT>f?Hl5Bm0ocFF) z2oUJn<97|6GtAY>gZQ?33UEcDqV}TL4 z+4GVZS)Ctg{C8{}Cqa0h1*e9(E&H~;wIlTxGxcz$5<<+GgOk>G5lgb$ihoeodYKnl zH~DFd;eM}r{N`18d@TdaC>Sru!=(ccE~e{PCcSe9;KEl}D!z1q7{&n)kGG=wa&&Xo zi_)&YbZ9#~%MZCcY*urchJJT{JVc#IgRmhS1wAr&!#j2HpAT-)yK~L#<5F*B{qp}t zzg^M)PT7j@O)1NWYu9_wEL-ZBj;hXrqQtsdxx*)-?|}BGt{<0oI$pS ziVJNUKz98zLUQ$SK_&PKYMN{%5=d)(x`y>=@v2n)7wU4!`hWVR8!`@GhKch2VK-X@ zyVtv(zcrM8#>zP&nU|=BD0YPfWT#N9paRm7Z5jM7gq_#@*Q!Wr6GfjI7n2Up9%qRS zDuvST+0ggZyVVjbz&KF^GIvu@n`^li=T$mMrN8n!`n8W{n95-Ux2V|E7{4zQ%$rB> zhO=8(?Czb_?CyvM&qv|pvlwzFLBU!qA zEHOQxv;Wncnii7xA6;K4>sFf+rS71%`RgF9tL9qk^L(A{sHXKN!im~LaG2@;+*8Io+5dSwnEN+!1 z3e%SyK8zOmd+Me3r9pTMANnhUK(_f#9REUNzS8|6>sw(Q6q_GB5)+|Gx6&A6?m%e`&w zZn*qX%q?Gc;>15qO~yY`8YMwoNv@ei6ct-6y5SBGFWvXDWW|mG6#b@P>R0ce7gLP3 zPGR#yq^f$~tg%(aV3K)j-Ui1;uqv>NQM~2ynp3o8@@u#T(h2bnW&)bSd4AB?{(fGG zt?S1d3CCTLu1nCxuqW%^{(9R4KEB1c0W!$;C@>pAS z!bUg6Y}Eg>)AMysL~9zqPy-Z4_GA#|rIIQP#K=8D)kB}Cfz7mnfNKo}{!5eXMKc_q z68`3mYIl1QSIWzjuj|2A(pvG;PaV+5&+lM6a!@_*b#4fW z!xgTEDNBVI2z+7H{Aao1W{P8J>HaCSfBc=`hza~*$|B2eox}FbD|;X|2I)0@|-l!lh18U4!vPy+pt*^I1u%u-2}o-!AaWo z3L#nIT?akoTmf+^P8%}qw(s{VHJuh5OL*F<77wU0l4@>%Z|19lMH3q-!kDf;j)BHx zyP@%cC-5BC!ea^ByXnD?)uH^%tMeWa;0AvAh=qdJ6vn`|w?%pDgyw%t=4R=(Rfz@< z_KC@jz(>Vk&W7QLpN7FQLEzSkY!Md8Daj;(--7HzrmMp%^;Z9q;XmJQlHtKR0+H2H z-_2iML!kV6w{J~GwV#>0vaPKrL;f==7@|&I-k#V;kN%~T-AS-wu=DN%x<`-z?54sFa3*4Z z%~nK-Po*mOquP=6MR8ER(&pFNi|79FR+DGHm3E^ z3@KvOy|BFSnrvFOm3>V(d4yrjIi^RcZ~h@J4LmXnSNe_j*>mRoF)Z`H6*Q|4>ZnbN z-eqFk+)?IL4Qk-M!0{KdKdXTYndFDzLUZ?h8GP*AV`1Z&$sVqim33;GC+d*WD#9C| z`-4wEmJdT24Ps+&nmJD@!U_y=1z`wn*W-g=rMWkU(=5_{>Whfi#4YqLxVsZH-$=Lg zh0uZosfh@;rsZ|g(_U9U!2-#{om!O`_H>mQm%L3!UWd>R`KE2lr2D?NkKS;<-!jB{^ny8b6=^Zhd0A?vs~3_HimGJkUZ ztJjO*s4)3zBYca#M+K9?i2x^s{f}x~(U;IIj#rw@!Y6Kn8;UM{%}Kq66zxobxOu87 zBK;Cz`DZl}IwCjY`z?=m`PQeNOa=Cr%D?>a02K;Rw2wESme){Ka@|$E z8QSTK5M2z7Icz*C#((7tq~UC`1gOx&_m-9d#@_YM^Q93SjgjU+IM?uq7GpK-~C z7RiK~3*P>P*3cadM{L3@6MKjKkn(LTyocFm#XKR=(>E6{tx4&c{z^{9-neI4{xfl% zI%(SDLG1N}1Akz$_PY)UxQ#Ybn1=4ADTccnUNL(FEE82~tLk&KFOMlU*+Dnf2E;uU z_DWlQ-RLSOmi@`YrSv_UEIoZ&W$YspI$N8jS*6`}O}$yS}2sJ*woi31AP5Yg>B z&J=|Nq-4TYCxn(UbK-hl)9%5c(kV8scK~?t*^Kid^DY*0zBKTU3i7>Lj1Kko@L+S1)mZID35dE+Yd`6uWep8}^NV~t$a170~dW(tYVJ{Yc%BJ-f`T<0IN z={yJ@w3?h^1Zp+%%J8IxnsPoiLJ0r1E6BV<4MgVA2R(0}<3j3NJd9Q{xQJ8aUAILf zWc~<#j!hmBrfc|LtYd4ce%#7Zd*s0P3`SQTmjV_hlMX8m9)0w@n6nhCo8BqH_S^1s z-5txYvNW^IZti}SQ7)X=m*Ov9z7&7$)V`bMxM2Ik!6jyvOF$E?;LPBxMh~?!&jdQS zHk5Kh%W?eD&Q+!^*^hOpTD zJVAfs(R~B@WrpK8UXz-`(6$AdCO8pCU3p?!>rI{ddG~8VS`zZd6{?Shxo!4Op}_PM zK$Ov&irg`_U?tJdz0T&ng}tx#j(U2+YR2r3em=5*kJeCR6cz2PNwherj2#n|^Kz)5 zH^!H@lp2`cHU$8J$Xa0Wo*a$Z2ZbKP`=MfD3~`4Rg)tXt`=MH?6OB%D|McxPOprU7 zJRKX_c|0zjQ=T1n{72`A4~sfh2oijmviGFNy^_^O9oS~E`v8)MO!MPq;nBJh@R<#pAl1hSnyT1l<$#@cHpTG?6YwIzx@ccACJ%_~oiAP>(+Gd~1#I$>7dcLoNY z;(bQ@d?emcgri8|qGs8ChG;1e?{$VZaXbW3x+}UNhi^Uov(dD@5e3KmA6-z(^RLQH z6xY}8*o?qJvAOSnI3g`pR4h4Sw+0xQ>Sz6YcnoqYf(vloTf4CorebZecKAgi+1DMr zawIj045N~*$Xp6uNYpOeqM`egOp7_X$|<=XZiUU?j^-JI(~ zPdE9~7}#lL$4}$&G@2@ZAQ;If2{U=}m^kNkFdp0D%U8u7rs*#TE%eAaqG$SI$pbXy zsbnNV!Pw#oq)^k&)Wm+<^#6gIlv(kH^7_SxUDLQD=#MAQAz>&DIDFbtrr(C@H;;yd zY-l1Lpm@GDFO*lY-fN8R%8>Hdmg~LG)H*2vm)@+7#?$|sf`tqOyH2>JooxDMf8T@% zjU62C?^MZ#d&QTx4E1md=@=%#xZE0zzKR5KlQJ?j78lcI3jee&j-7)d4UB2fy5@KR z^5!WFA>b&i)IaKatvztfopzcT0JP16?AYHzoTS^^zSOxO$F1>)&h*@QEnmpp>Q+;K z1Aw_K=Q#G;S7_}&`o&B2-H~{oy(!Zmw;Tkh<&YlE42z#%c~8ewp%dkOc;1J(>_jJc zR;>L;_nT=n0dti!7xUHLET=4YHDD6q`UKVAziy;`I6nDu2JjN2`5#?c7vfNj4vBbz zKN1hWc{ff)!Tu9>bkVK&iq_6RVa|EPW6371)G+z>{a{(+%xUd8J1oo1tmXgcxCZdE z0FBtCU|Cf7l1_^tdT#pdpjK>t`kzE+U3*M7Wwvg0`=E`71-pGF_ijD(J4x*^$dmiM zTR>wdG058Hv=y}VD>mf}ckxr|0C`Y@vp+E{88N@A=5iAvIIY<7Tmy5MaYbGb*#ZA- zcx!7dhuc;qN&EW(@F(cwvO4eqUwV5@_G!nK;9Y84DA0mh;1X;-NOmr|R#}=7*c^3< zQgJ$xfo>q|6Bttqu8y#3$LfK~bIGq*3+frt_+%gXS^p6c=vn3*hoIJ#xJ&KCOJp<^ zt7_DI%|NsJy=`Ki8gmP`))E$+>k)WnTf|Vdk%etPGnq1dtPd0t2IFL%{`QTg2njy$ zi_DhV7RK6!BzBAn?e|*fStNARA;%XmCEPIPENVT)q-B%V}NlOD+*g>gjpZCsNX z^LD-8`P4}1Yt6f>lJ|NlW&eEv2G1mliKf6~EUt%|;pbitVqsysVNWNf83Gu_V8mAS z>NEWY5Ih)yvX6?^R0`C>%Nc_7Sl<;Iz>*nzxBZO6TX{Oc2+qm%Zz-Em<;gYIptxu~M-%{mnIcaw z-(`B)UG@8ZDS6Z_=NlP&C^J6GkjmF2_R&=jB5j>hys~ntpQ>`!>2NTi$Wa-?V!IKl z&3j)`uzN9>z;&jSAX0M|NDs^`sXG{SEE2BbdmtjIuPA0u*9I~!7|M-OLu!)_Xo)4U z*57|2sv(blTjU@(wN)(s9N~q2Ooldf};ZA>@&m>&m8bmtbu5wex?P zMx0r3x%0bK4*tJ~vKlDTleA?TZ`+NPk#>`-k!=XC`K1j@vS15hBkRh>`6mVQBZCgN zFM>9l+mvP&kNVhus=DQ}y76b3$J6)!##fjK^@FrWxli($hW5Jugm_Yjoj!O8H!w zX2GE7WoKqx-_Ulq0vkB?N6@xMPft~IV~oeHX+Fw!*vGN50F+tuZN<(n&l#m1*9Byr z_bpJuGMGO3m#}ZOPI;8Zyx)GgVDoQVv%?~gBMd0}FIM2dmq+@-@jU%=AccYuD zSBF+IpZkOi3Orqxt009A8W@ntsGqINM()#IBmBBbox|KEUoYxX2KV<17DlvD|3;eU z9O2`%xaA=Gqe5Wx-pLf*Qa0mMrz#$FdO>2l0-6t{^wck^9~zq}EeFTvVFZhhMqJhB zeWM=<4FE)s`8Z#)v&5y)1G%vTU$6%Hs zJ$-{Jtp`eHO3HSqUb}tKMoo{5JnTG!C*wX6XdGd%hCfu-sUJ)JlVT)6G7ut@T`jiVN0G63MA^lxq9qQYbR=fkl{7^gyV>?vAeH*{bN^slwpla6|WV2rPgBYl+e)o2l5K+g)ynfd();2v+Fq|39dD)S{N?IG5S<>PEMudrd-m$2 z2f4Ek?f~W}Rt8g=QNj1?>*IiD@ ztDl-0R#;M0GF4f{`wo<2NA|S89RNS!RASiFl6}SY+6bA&K~mWrf5Uc2%a!#(rUUux zl{Yhtj6~H#}QEeg=H$G<~q)Q~fwf zIvnqI-oWZqw6#P3g2sR7aj7L}t{ydSc+p=bL%;8bxZ=5vk7{PtE)imMcQ=f}QD?A@ zV-13O(m&v*2a7fQ>v6~R{g)LB7h1r*TO@M-jdZ9CTPsQeVNZ98!6`bN5r>Gcpb0Ut zT|X+_^~Y}r`nYY{bZmlVl_LlJ8Wq9^CTSE$PoY9ZFZ=vLS=&Rath!YVdwd91v>&2_ z%lLA8kbxljKO$;Sn07Cgb1Ny6dz6<%x|k2$1#6Idt1T5(OTq)b`4~5u7rZQf@H?mG zY1fJ=K3_Q$O(yj3ztb7KC_NdFS%voe& zD$veAiOs$}E<(ld4QccRtY5bND7BnR9DYq=raI+Dujc_UPZz+E9_I6<)p$Ib{DQ0RMZ>JyKF8WINR>%dC z?WLoO$hJ|3<_DwZhfMXEfI_DVkioKOq^Hw72ZdFrlRd4ERVta+ET62li? z-xcq5V!hn{W1FmptF?T1>c^YsS?M~uCeu$ z-CEiU;JKiQW{+vQo&V@W=C#8ep{dCRF%b{eg7ADXldNoV3iIMCgq%)1Tk;gCUjHvX zV(dbBKa|>DQuc2b?Bs05aXWH!Gzqc0d>{l$ub37h!Up$ReDDbFlX&rom2*MgIBXh? zPK(!~IZ(-38cM+l-I{(+5VZVHMY7E$c?dWR$X+~@iru-&A)++*-b7)~Wi(eY<8TtU z!BiD?V3IZr5ogS?)Aw3=qf{Bg%zgq44CI$ zUQ+|M)#R^Q9%tvS3o#^|zd8F|1UPtjP86(58ZuF8UJ_sCCpPvRw#=JWpU#MR7eF30 z=@Y#_Opx(B2mUb{CLEvRb{x2~lqgADk8ASd!;;MEJ3Rf5Ttm=jbHRPYHQT!>W*@ll zykNy_W`c0OV%3`U7@|8OF>5fRv|s&*(KA+UA(!O}7R%AgotZ4D$SLCp`RZ+RL5wpl zYkBPEqj`&=-*d~VI9k^ZYfbpv?btj2%VWN==nuzkc)zzT))$$z>o2HyFG4O-tx=-B zfLAd2X+2cd+O9+1#Didyf9>}CDBcPj-EiH@d$hFoE zJ8TRiuT472U=v${)%uUmjv;1l!yJC#aaBT*ZFZ|wb(dNasyqV~&||kSLeoBoF5U(A zB>MAN_zGVAAcf>2EZchC;|SUt>lQdlY6c=+C`I4ZEwuyUoQM2Wk3u&=-0T81AM zHP6jmGRywYpQOP;dNHk-17_>?#@tk2V#YWwlZCqVI^%gnBRC%;eL5FBbkkwg&A_mU zOn*B=z2QwlmZE#*fy;Awl~zvaPbJ^FH-_{RW1YZx?P|MBMUCw`M%*&-|EduF)g-g$ z7Q<&&VkjK5Z^h&c7q}@gQauUV(M)E5Qt65l|WUj(J$6$x~$tr+a+(Bf)bvs_3V}Oj(h@RLsYTj@uV5V#vB(!%MJ#VYzlF7F= zxnp<1_7Rp&cUMOJ-5LCAx1Y1c20Aygxx^PdFx9U{ev+f>$^PQ%2P%h0xSkfDw~eH6 zU^s_MyPX3;=INa7^BaNUF~zxKZ4({0SJ|E&fiM>ei#6Yl&AyWt?k$DvPEmiI|Dq`S zg;Re8c6~bpdrVBfmH?{aCH~!nK*tUi`|P{p=>)?sH+v}Q>QkKa0|Vhs!M4LV4X23_ zjj(E#d&pZPs@5v^Q=U-bLy#Gh=ti1b);*3+O51K?U_sUDx?;k_cObCXGPY}&mvy3m zHG<1!rByIO0{X?$Rs=ha%wN>*p)%fgA3cg42&krDMV`wCXhROb+6X(qyPl*xnKw~b zNnraTv7)bV&ssuwh2R&wc(HKufn}iaD}V0tfbmd&O{4=5t+h>D^y?&upX2wmr=ZQkp(?a!}a@!k}D!b3QNZq5ZX* z2e_U;!CM}l$tV~FZ|oCZFe)(FShegS>7ibwgdp_+Ctk{9cp-Sad^2)O)n*fDoqFYG zlGTAoXmhL?gPnxVNi{B(9_0Gbb(uo{(Kwz`+uxM&@ZBTB^dP!MbK4?c?F?#T*6z%5 zI!62(29Q`+@2=QOQ+r}vTTEqn?l=zf&u1%Jzqr5fUSTt-+uIr>rz~2Qg?y6wH-&Q3 z?j{92_UUaKOEt`nHCGzW_Zj73D`hy=#Szk&WZ;XtxxHIJ^6Qz?ueTlh%)qpw7GcL$16&mt*dEOMLcZ2RA*VFWGLz-ahTEEqdtzq7qYL!8S z9t>vIrG9**&sCSW?IfGtRYSrL>mmA^Gm~5*b1hLcC`HKjyCGbuXr)j8exs?BuV*wb zTeYb?+BriG3pr+!!;+e>7Vr>@*x$?72T3`YW8=0!A1?~GR^5q8yi9VFh&l1m=696M z2Gd-$OL4H|V9}31A%Z?|eF*QhEs1aQepUCV^7HZ$6U8d3=bj%Dud{Cq6=veb1pmlo z*~oq!nB`;Omyp*Y-#zm@L|AXIiO<9ql&8qg)unPH#h<@j1zebFovmJ^$&!*qxMtk* zK~Iy)76XT>j#MjU+(RAdl?1g%r?T^XlQ4R$;wnJKnq5bjB~A7iD?vVNn@AwkvEJ<`>1$@dkU6HYKz(P39dz zKZH!SW5<%iljZlqNuO){CS|7VVAVLGfS$Ns?xIVFN4lb`zW=(nGw((aA0u)OQ|qeU^X^W6`wjCBM30P^ ztWO(eP0rSw5fRue!4``{;KnPoOJ1iyyBa1>x!g1l%j@E?%^;reM|n zqCz0nqv1~Wp?+vIlI;Z*dich!!b z0Se=z{QTygu72qOrwnnDQhdTqMEUNM9!V%KIP=A6iLe4<*fxQ#z{lgTd8v`Enoli| zr+>J2sVk3SKHsQXB`+gxw?=-P6MjEdGR^alu1Klk7tD3@V>ir;wzyq-~4|OP) ze%JKoKD0{eSY02gr`W7E7k--vsECQgc8J>9B~bfa(~zSxv#zsO`o3PnEe$JEH=a)CF zkLysYzQU38DH@!*V9gx4fbJDT_et#zs_=7Cw}$;EPHD^@?XD%8IjL>x3H2=PFLl^V zMj9O(>nNDWOKk)^5eg$n&=3YdmHIGsIqTP=qq&E9$P@C#208$fN!f^+A1qeZs>%*o z+?R1hNKQqf_p*ABCW>ZjDv)|K$ObR@r8=+y0+kE+Y&;5o*!s9G{N?I@bT5`!VVi6y z)!{Zd(==Y6*2_F{RcBkFTd(J(HvQJ=BkpQw(JiAtq#TqOK(8RCl<$@_%NMs8*n`*a+#-4?37D`}8+9FTul&`>VzCl6*0xfN3{)MhD?`!#1=q)aHIG zm(Qzfo|of{lfahhKRX&Qt8=cWd~g=Jk7%~Q8tjg?@OVma4f;*eZWpv|oumld)D^CJ+QR?=O z4eGHde%D!7lU(iwOSwIS#Ey6~=Ji`RL*cqJLh@*2!gbZ#Iiil;mvvqNFj zxu-FS=$tBJej=ps+oZI@$4uFFAy|ygrBK;8_V{1O+U$=SQZF(?#U2v?2Rv2ew|9*8 zzuWHG_l%l6y|lif;_bD~R#WS;P}<@7pjRR2@v77Kb3m~bLGR0ADx9tH=TeV@?pHC$ zjIY&{7kT1GiNf5ieV&4}2b$!^c3vtN4}huAzuwrl5@ns+kdpWERRL)g4){hM%?uy@ z>OpC1C&{>C6R$_Reh2}=i-OYWmxy_IdQiy>Q*R2ltt1&1$p=f%QXKc+_318R?+`OH z|CG##wxbwkSPiut_#|r}w;m=U=DhciDhu$1m=MOZ7Jmc3ZFgcWI51O5t2!{0Ewjxe ztZ#x}I0esZs7p5lHDtMDh}VNtFhEzuY#Y}lnul4Y z{*`LHSrs|p7>;9&b1$gssz(c$7kzUy+17d52`lh$E|2z?FmoY;MfefN4}fO9rF|=* zgI=mb7M63rD3`7O(J9PS>>qvDq`_rqFVmJ6OBc9*8nk|p?+=-+X1;rjTpVs@Oe*6?*hw6 z5GAVcYF`a}%~I&ceROWQCrk4=g$AC@of8Rj7lH4`G&BZ0903g%6LxL?(e(oUY&Gm@ zc?r5L{tAw!3c4gdZA-eMl@tm=q@j_u}(-g;Ny_n za?V8C%k%c{H?vB1wY)Uk7L$h>1kZ6K_4fbha^qE823jh$pB`Q6G0qe zCPFqZoG#rq3iqU^ga4z;`HyZ(_Au6L09Nv=_J*n0)jEb#&D$AK^(htv~MKuRV2%Di2)Jpj;geZD>7v3_9=F*Kk`7TM3!# z^+Il*Pjqkp6Uk7;cd7Ir-KN_mIq*kXT}axEWo7TRLfC-^T=2bEZ-Iy=v=d%_-IgYI z3($0lt4#JNxWeC@?_36*R@|mty}6uJEBpBDNZL2teOoSFupGJP2!4bATQoMC(p1%U ze%{6q*Pn%clShmY&@Vu2nWJN5^l#RUExt}nF8_uO;@6iRk)*I#f^?4Tj%;q_9XJlW z$FQt~IHraxaoR{*G)K(1rY;qV@9~16T6(QZQN8}j^!UWiIf1^3VFuw;<6EK$-V9RC zi1{8mf*F@tREvGMLMJif=5Mp~+LtVS;oLKcjUK)kseQI-gR|elcCKu3+yx)iuW$G1 z%;<)Vjf+Rn`_(TBpBWss-31v42l<&mh|jA$l6g5IK>gBmw`;g*-e4If4?Z@5qHh$J ziDy5!XOVT^XbQ^ZAn#m(#F&;K6b=;5$l=rv1-$ezO!M%Gkq2p*3`2>KE>hVx8QE+- zPTWENU9ab^>B{3KD_<+ZMOvIQ5$7LrX+9*f#&BPo{p98!GmD@iBj1>P>Q5(W?<0j@bCrgpec|AZoKJazV`~b}@_?+9Zq5JWuZ-I&3N%T|phGd~* z&!__B_&ncBx6@GCbj$6m9%7{SP2BzC?xzlYwt$>UZaQH0rZ9|;j`#gGQ`HxoEx4)9 z|NdM8tnrk2EyMJ8U&Ntvz#=h~@GE)%qd;Ke8e4<Hs`dIr@*2hn zKH(%LLyT4VU$QN1h`NcB`SEUG?=ilR=^U#Ek5-a@zp*IE>$S1i$^ws*;Vh@TxWBy) z3`zF)-V6M(RD1A_+h;*$p>eCDQ2(UX(b!Uxh?sN^)RJMNOHW-hDiCA#^swQ1pqgZ5 zg%|EcgNd13KKiBupOTX_7jRAkk?(%`4H!6%1In^sdVh@t<&=m=RZ6*z*Y}YMblxDi zDb3OOC0J4b>7E#nA1)Un1x_yH@{KQTu&@`A4qI4%vqH{2$tA@0Rfly9Xq3G!ET?yO ziT_z<`mO~fx0e>!ySCWNCM9iVegFy-Qy@e3tXHijQ%p{=abi8q_n?*tTil7?6$$k= z$W{m>y%W&FrwXRF8K_^g9BHNKhD*0==_|%@+q0#6+s)~yaXe2rAwiN8rtuw@@G%`Z z%N<_mPn6sqxTT@|<}}l4+y}0PTzg*=?0V+A16OW(?p6lhGC-A%O^uZlm+vgEs_uVS zwar77@W8sl$cHUDZzshHy(=+VOJJRY#zmxfxH`~!G>G{qFQ8x*C<@HT$L7R*t~omD z{DAK_ENIP7=Yh7OiUca3(&bZv=V|iEhCA6%=)8%);PE-vwBqIbP z=)c|v@qw%j)W2Yjb=2$VT4hcD0ZsMNe9);tHvUo!{==iKWOhY(@PhB9cS)AJ#9;ln zcIH9YdST$bTNC2LJ@%_kcTN6wMH)utMarVq#_Yh1zJRh8WKUv-nk28mxQB2H5=gUz zRWawh>isn2oc?zQg}o+FVi#D#b%a6k7zOkl(WS3v5WnXwehsqrZ9_qo#_}>$2oEWL z!|Gjj*(gIX`cz{^5?QAvKqnQK>tLwQGRxmM&O7c~=2q6g^y8L~p7%V;k7D-QT9ChA zrVRlHbSL6`y)sPU^7r4}XwO9XIY$n~F5~w@LJ$9=8z6akzg?L@?PZ9vObhxMdl(Y_ z>Gf#ueGgy)Z}N-GowA<%Ul>R&phY>fEV!8Cz5LR@Xil+XJUlxTm6w>P3|cYvn1{$r7l~4MDm&Wk97vc z<}6c9LI3vv3bWf1JA-Ae=e6JC?JBMopxj^aO~-AY$U%LvUK0A!y_y_$wlhWhGC~J2 z*&gl`NtlPMe)OU<;w39MN`Xw7(b2ym!s69bf2}{TC*=^K*okhI-th?g%3w=zSRkda zUN}4|i}`-Q;v@r067!Hlkrx+WTpatlo?Hz-JgUfaXx4SKMTGFT)@M5mvrwM^_vwKj zTl9lz9fLjY>Gr4^8<>^lx>sQ{(AIX|^`ovNdw#3R&L|^F)F&OA3JK8h<#YKj=8PsDn#2QkAvF4WQ$I>m-f}p&!f6+h0X16u03_*&y_WL5tJkx;8#1pV;wJK zxZtdujo5tgtSkt-q!85BCdhaQ11J8)8?CJknne7=)*s{NazfZ0p0#id_i zjPG{UCrOjX#(}bx6$|W=qf-t}K2Y1qKHmbh+ zu`t^<2f7npkRn!h@*&K)C7@Sv$rf9Ztg?>%sT~^whYm?f7ygy}^#{`*ROa$&phLc< z|A~&KGo$Iek;v{Wcg+anZAF(}Fx3T>Jp;)_VRq>zwC#_TFco zwa(i6xA!mo2&56Vub$68FC9E(lTsLm35t~2IHOTLs=UY*RSjv#rJJ zR4Z9v6U@;k74DH+E(VB<6)G&*eZC43D1^Ok08f6``T*vhtnmBLI8gy96kFP@{eIm2 z_1n-NMeaM9hCO0pH`-(9eRyXKc)82?N6wgK(hLE2)Z-#F>d)WDt2>!O3cYFO1f#>a zH_FpjvujEdf^_1Tk&c5Nk)HC3VlO~ZBiBqWTy~pg8C}qx@dG3yf$N=`z`)FL-?D;qZ6_57@Lo8 zO1=;ZWC1$NzVYLj{E!80aPSSaTDR%OC01}rEF30Psv2ZW22KN^A<%$M*GuI|>mKWl zpDXVdtqP@Q*LmAWJyzge-4=edDQ}hWP&3yLA*Gyh0sj;C*n`5q8w9xlHCB7q(^~t? zfOBX@D)4KuqgvUzWg}axrCV3@^^dynNlxghcDvZV`CR)U2G)+rzH@!>sVRx7BqY^iiXs%|IK}`j!nWEzKyv-UkMvZy)%BpV2nf5# z6*KaVc%|xGar*3K-!(ZS7*q@>P;FCHG4=@K8W?KzK5bP7%#!#7tRt)sQr%_*J2~S` zimwmyO&(s-x}3DbS#rWyC;4%sMzUWs) z{QP6=dtd$HA4DV1*t(XaIE4Mmv|5dD361lrzj&8u!0No=e}Rae-OQdt6qc@`t6Z5}8V1cY9dt_D6$1vo@9JUh|mf z+aCO}djA}p0Lb+%4F`q_M_yE(m}eVds4V7?IxPc|JTRl#r9p zveudX0lqL;a#Z8Kx%S>Xz%!~=k6|nYdCcT3_Z-G`Uy)x$B`joF4X$dc`EUzID7Fcx z{`Q(ZURv>1Q%l7LTUEy9+&y!4>u0fV+l=*ZE}=xdh3O2qE{v;T0Ah$UDjssFa_iCGQjdi~AbXy!l&koDbMpqtIQM4lvZr@W> z>OV8_a`wZ@(ZQsrV_TGsm~&k9m`9pI1-vVuqiec-pmY?qR8{6-6X#eDQxVN-_sG;= zOb92Xd&f7Ge!uO!pga?mxG%QLOas`9*~gXk8)-oNc0(*!M(?Uv*kv!OU5YVcT|U2K zcc6XDdSbyXobGsF=PwNKz?J)zsm0GbX_;7wMwsjzuWOdUHM8t2wdd_KyDh3FuP#5R z?hw1+ENDQ;go9!3*EVQQhyMNZ}JrLHa zT^UPae;m&Q_7O8frYXE&=r5~u8F)Y7`nawE{`oU>lA}=uxpWye(*{0zj{~Txe;SRS z?HnZ#j{o$uo^oHHGA!}YD?4xhuq+Y#%`bkqwk6Y1J}Y;x(@uA^)eqlr-RpDo0{EFa z^g@v-rk~q1LqS-i;=T;b=*^bb$92&grSP0`#AiM$2%jqNbB`;#!^cY?kV%N5v_YIC8K}&aF7a z*^Ye77ir#_d0Irkkec>`Erw1&3vD9_m;6-}0&0hYX2b`P*Z2JQc|fHKgZ;|LvtU@a zDMtAZ@7>t2f10Gd<)G0#lUt}Gq>lAOe9eHQcbyyT8&nMAX>|Z_o=zPGN9-8h3jBk@ zHzwN)|II=Rs~fFuA)x#!)?`_+KphFwlORgZKNEBOfd;`CBF2!=Pb}3D{y!I_^B`u{ zyrEz0O)`bK7;4yXEb`FdIglFOZH(%f!nB0sIl>3*_HcypvD=UE3A<6cB#|793l9^U zWZY`|mI{8i3Qshc!?HkGe*r#KMgu2=-Lc$mHrE^H2hvsgn;PHmaYD)WH{Z>Vf!F~% zPI;Hl=1xpIafYy^Y(#im{{lZl;HqW>=uSUgK0%ATF$|A9Wp^4l4}MmGN$mH3Ke+?P zQKasRJ5DU4{T=bL(Wp&&E91N(528_Wa`C)de;|#IQpz}S=QLOQ_oaYAfhUR{?cSW{ z(6RkjO|`9-uk>$R!aq8iVtZDNWeCW=9^u`Q8T1UZ1h|g8s%-v!S!^u*SQE@w?rxL) zQVsb)H8|RXvdYLGx%AsRb>z>VNvdaxgc9YdW_A+_=jvwWpJ}C}45RpA*l%HLeQx+Y z=-!4J3wkjC^e$K^I_-lpScN^`K&;rMu&q7g_*pQQ0r_pkg&E|trKvUbq`7+8t?Sbq zdp3Qk-n+pGfcYPp)sm>+t_-XpxX+x|Ikvt!hMxeqGfu|ulq^cc$QhlH{E^{CPp&RW zv{3`-=$Y=93z+?^6WP+}7L{22VRxL)xJV@jzQ~uX(8}*$by%e%@7rnO(G)n#>4;_A zU&__1mgefmoRjKlCKf41_^^v%-^sU?%zqU95~+zEo=)9evpT&+?0nRC_EZHDJva5@ z`K#SqJh$xa*`({$vtJXf-mu$U9h_Xu!j59I(Sv>~aI+v+n=%!skoh4B_6rc1bQQmQ zg*XmHnrJKWy#W1CKc-jDjb7(a;aJx+S3fzr8a1k5K%l?wnsN`_y4e&az4eX2kBhD> zvQQVw{Bu~Ea4Ru;h4=);ijOV^{>;+^x29GOW39S;On_^gHbD5 zh{O$8?nGs}TJ7WZUVlH55wO_PHjG+ipl9OZ#dwC)KbGjf3udrgj~e*2!X^I^TlmKU zzM9A~04tuz2t-kTl*;8JFP|{d;ep4-S$#tLSY-n{z4wu|{r7Lf9`=fGQ$Mapp1c^j z!;}Kh;cu&7pB2J)Of)xJ)NPT2%w(2O4^yI)u-;95_L)X2SX7sUwxxK->Z7O!CHs{z z(fRoFEtYCOLi-Q8SXF}-rohr&vv|?sgo>7P2y7rBkfkQHxx0*PYHIcg)x-{L}20| zK_7+~lphS=1^biH#g-M+My5qEc&X~mpm4(b?NhEM zEsKQkMpL*xB-X#8;Gol^9=SA`F!0LDsHy%duCab+(%#=|FU4l#M0%e+RLLRktZ>IQ z{W8OgzShgok1X&YV>SVK8#LiFcIVdNFTg@o#2U95zKT)4Q8H$*Pir2~GAaMf7jloI z2+C5FhyDCL$y^%4)*m2e(mt-qc}pGTj!IYWT?i~NfF$p;KJ#MMzhe*QiS$-KYdqxt zicg#rZln{Pj8!Yy;CD$eS9b`g+G~UJ4N$nL=UO#_KPPh_65ekWkw?^I0**shd^hG* z+n-=wMYP_Yc;n|pazJ|JOdQLLZaJ6HgZp~`6AYb}vUv9bG-HIJARIke8_-Efx9 z1j6911tIHDTXk&-g0kA+g-mmQ^Ie=)+Ceab9-O0Pycw5BxX;?S#BrF+8{3Jzm~)bT zGKm^Ka0wfh*DygHUzK82nPF<4?GZb%74D4AnNmt4a{FaOpXE@~ z_jX@(cP6 zsjOV+&B~bN*xIt3L{WUwUZ!1K8cQ*&rz`|@S&h)=;H*%3Lz9AaoSv7LuLYXg z^d>+5mb4#f;x(A+>t{%nS=G`K`5xq670ufXT^96J)vS+KB!hCML4lgjwo&nXTZV$m3JCt6m+%5OzwYGt$_rv65wMRZ(5JVXE&?5W;J2~V}i@T zTZdzs{P}6@z={8JHMyC1y5ozRabkTX8po&$U$7x2CY`O zrzMoqs~SP>Iwh=2A+ZT zi?$=d0Sj%-Zd#v|#{u%)pFctt^ivW~ovCY&#AV7@T*M4_SRG_7A za#n855lKXzPBa3!cOXmdgkgCxNkjMpReIoh7r8kOt*%n7^(da;ovw6`Qu|DUWEX84 zS#_ycoT+-yO(=>#Mlu{+mYxmi%1bA|%>=K;X5@Ui+uc-`+0?sJ+dM8>CAk4U>I(=^ z?*n3O(yL6mUpoZqFdaUv2(CLop#*yBX@&jbloU-!qW+oRh^9g@FA31z@~xb14FHlE zAcTk!h@o5sEp(7~nu~tU2ybnzxIM`j51Fw=d=cO%zgTnOC`x2&b=%$e4Ob2bl=Crw z2ex6==)(>Z1HfJ$xHv2WkvpQ@kwQ$y>Ox@;!(7`cc0yznuy$?^DSbIY5n-CzM_C)2 z5{ZyUhZQg2YbIuO9NAoUhWEhWB~1uKU{#9*xi z{+%VSBX~R2nkU4b$vtwzIgJ?>)xtWNTJOM=&(i^(O>^|mptc@L4fxd9dQKw<>_n^{ zVvf-w*KdY6%#4z%?_rbGBXi`~PkOJ6vFsx~=9@~LQ$sBBT~}S}XoB<>jKU=OGYX&r zv4O~r?`Hs!yz?y?KbUb3Ks>dlcS;s#e3Bc4B*3bGGAszBbw&(mr6iFx_CaQ`Qfc}G@-}e(=I`3)ya@r$$nX2^`N@qb~oA69#Gg%*}INjYz zjXi1vs(OCgNUD^D#fW(rR|Hm!1!*MkQa_}2&sV37Pf0tcQyUGPQsysg1kMwXVX@E~>IX5lQL$qza)9B;6AV^r1(t+ZM7<4ctr7kkoUsli$&iBxbQ?>UT zuXVEE?qI8w$!sI}4}5({o}H(}x<+UHobocS=t;Gz)SY}OIYE3p@9e%Qt-R_MMXaj{ ztG)gdU)lU_RaLpMwWm9lx7)uYB$b-u)ql)d@>4jpiW|+H+~-&jr!m(1of~c6$O5Wj ziH(^;B+GxUR8Sj>D-i;Ur&pChfFLvpMJbqJ(xshYUBAS$f1C#wpB2&^{ z9)>qokxm@FP&I$+o(o}>55*5_ZKb|8n6X~?JW}hC7AsBsx^HZ-r1-8`C0G`ImCEc5 zDy&7s2ImXT=?7k!6g@Zt7z;GpwM)Ir5pt7jpMCDiqXffkS0%Y%D;R54p<#>SDv;Sl zYnve(MW?~~enT?G{$cBR=XotTEgy_kbwuBU`@vWc&#suNE8zl0xeP1GUZ)3b@wE0z z^fL0VvHbbeONpe|HdQjS?)d#O_tp33BQvFl?G>ju0p07A%ds6G~#tXVMPBxgmdescZ-&$uxy`7-^VYADI$|Dfi(-B@h+ItGoNa-&wqA3p>M_B)_o@9J&A68zil|>2QDR!$%&tU<}j#sab3)G%%tm9Vhct zQ&W7MGqA=eO|)#zVRU_@(Q0Mw^2PxdfA?ZmetI_R?ot{BfycfrTNbsR(Yl^`UU?Zp z=L9ko?;gg;8OKFGlj=;xfNQP+*N+4YK=f7uSd-@9POh^Y@6a@$jA$Sy4G{X14oU%;pN+|Aa>PN+q9f*-Suiex;1_QWK(S>AVcp1 z{JB@zdh_2*dlV0?{{q-LG=&zk?KA9#<`i6D%d*oFtCJ1eotFjniHFZI1CGSSvzF2e zNgFq=WYghKw@rM00nCg}T|+lPKf-93rDQATSpOz1EzJ3B1NHeZgjR7KsQpo8h*D#r z^-4Fmq(fj`r%vjn->jBJkB&~TCD#L_5kIx-k+`j8QHa9zdGn1j1FnM)2=Rk=|FawPA?5V{n*EQ(|1n(s_fP%zY&Mxdk29|Q!D|CjU7_OEKL*!5J>U7X9V`h?U^$gn%s#vOl;!{b^xVx2%wD;1KmN}r1bXn;?AO?T0UYs(MgRZ+ literal 0 HcmV?d00001 diff --git a/examples/20260423-presentation-liss/img/coordinatefc.png b/examples/20260423-presentation-liss/img/coordinatefc.png new file mode 100644 index 0000000000000000000000000000000000000000..94c5426126642e9b5eeb7cf96069196573eb500f GIT binary patch literal 58876 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