diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..45c383a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "monthly" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..50b2ad9 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,47 @@ +name: Build and Publish Package + +on: + pull_request: + branches: + - main + types: + - closed + +jobs: + publish-package: + if: ${{ github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/v') }} + runs-on: ubuntu-latest + + steps: + - name: Check out repo main branch + uses: actions/checkout@v4 + with: + ref: main + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: 1.6.1 + - name: Configure poetry + run: poetry config --no-interaction pypi-token.pypi ${{ secrets.FASTDATATABLE_PYPI_TOKEN }} + - name: Get project Version + id: project_version + run: echo "project_version=$(poetry version --short)" >> $GITHUB_OUTPUT + - name: Build package + run: poetry build --no-interaction + - name: Publish package to PyPI + run: poetry publish --no-interaction + - name: Create a Github Release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ steps.project_version.outputs.project_version }} + target_commitish: main + token: ${{ secrets.FASTDATATABLE_RELEASE_TOKEN }} + body_path: CHANGELOG.md + files: | + LICENSE + dist/*textual_fastdatatable*.whl + dist/*textual_fastdatatable*.tar.gz diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c41a60d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,58 @@ +name: Create Release Branch + +on: + workflow_dispatch: + inputs: + newVersion: + description: A version number for this release (e.g., "0.1.0") + required: true + +jobs: + prepare-release: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Check out repo main branch + uses: actions/checkout@v4 + with: + ref: main + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: 1.6.1 + - name: Create release branch + run: | + git checkout -b release/v${{ github.event.inputs.newVersion }} + git push --set-upstream origin release/v${{ github.event.inputs.newVersion }} + - name: Bump version + run: poetry version ${{ github.event.inputs.newVersion }} --no-interaction + - name: Ensure package can be built + run: poetry build --no-interaction + - name: Update CHANGELOG + uses: thomaseizinger/keep-a-changelog-new-release@v3 + with: + version: ${{ github.event.inputs.newVersion }} + - name: Commit Changes + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: Bumps version to ${{ github.event.inputs.newVersion }} + - name: Create pull request into main + uses: thomaseizinger/create-pull-request@1.3.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + head: release/v${{ github.event.inputs.newVersion }} + base: main + title: v${{ github.event.inputs.newVersion }} + body: > + This PR was automatically generated. It bumps the version number + in pyproject.toml and updates CHANGELOG.md. You may have to close + this PR and reopen it to get the required checks to run. + diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 0000000..00e49c8 --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,53 @@ +name: "Perform Static Analysis" + +on: + pull_request: + +# will cancel previous workflows triggered by the same event and for the same ref for PRs or same SHA otherwise +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.ref || github.sha }} + cancel-in-progress: true + +jobs: + static: + name: Static Analysis - 3.11 + runs-on: ubuntu-latest + steps: + - name: Check out Repo + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + id: setup-python + with: + python-version: "3.11" + - name: Load cached Poetry installation + id: cached-poetry-install + uses: actions/cache@v4 + with: + path: ~/.local + key: poetry-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-0 + - name: Install Poetry + if: steps.cached-poetry-install.outputs.cache-hit != 'true' + uses: snok/install-poetry@v1 + with: + version: 1.4.2 + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v4 + with: + path: .venv + key: static-venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + - name: Install python dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --sync --no-interaction --without dev + - name: Run analysis + run: | + source .venv/bin/activate + ruff format --check + ruff check . + mypy diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..14e1421 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,88 @@ +name: Test + +on: + push: + branches: [ main ] + pull_request: + +# will cancel previous workflows triggered by the same event and for the same ref for PRs or same SHA otherwise +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.ref || github.sha }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + test-windows: + name: Windows - 3.10 + runs-on: Windows-latest + steps: + - name: Check out Repo + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: 1.4.2 + - name: Install python dependencies + run: poetry install --sync --no-interaction --only main,test + - name: Run tests + run: poetry run pytest + + test: + name: ${{ matrix.os }} - ${{ matrix.py }} + runs-on: ${{ matrix.os }}-latest + strategy: + fail-fast: false + matrix: + os: + - ubuntu + - MacOs + py: + - "3.11" + - "3.10" + - "3.9" + steps: + - name: Check out Repo + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Set up Python ${{ matrix.py }} + id: setup-python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.py }} + - name: Load cached Poetry installation + id: cached-poetry-install + uses: actions/cache@v4 + with: + path: ~/.local + key: poetry-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + - name: Install Poetry + if: steps.cached-poetry-install.outputs.cache-hit != 'true' + uses: snok/install-poetry@v1 + with: + version: 1.4.2 + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v4 + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + - name: Install python dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --sync --no-interaction --only main,test + - name: Run tests + run: | + source .venv/bin/activate + pytest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b72cfb --- /dev/null +++ b/.gitignore @@ -0,0 +1,167 @@ +.vscode +snapshot_report.html +profile*.html +results.md +Pipfile +.python-version + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..e513be4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +repos: + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.5.1 + hooks: + - id: ruff-format + - id: ruff + args: [ --fix, --exit-non-zero-on-fix ] + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.11.0 + hooks: + - id: mypy + additional_dependencies: + - textual>=0.72.0 + - pytest + - pyarrow-stubs + - pandas-stubs + - polars + exclude: "tests/snapshot_tests/" + args: + - "--disallow-untyped-calls" + - "--disallow-untyped-defs" + - "--disallow-incomplete-defs" + - "--strict-optional" + - "--warn-return-any" + - "--warn-no-return" + - "--warn-redundant-casts" + - "--no-warn-unused-ignores" + - "--allow-untyped-decorators" diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e69de29 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..02024ec --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,181 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [Unreleased] + +## [0.12.0] - 2025-02-06 + +- Catch overflow errors when casting arrow temporal types to python date and datetimes, and substitue date.max/min and datetime.max/min, instead of None. +- Format date.max/min and datetime.max/min with an infinity symbol (`∞`) when rendering cells with those values. + +## [0.11.0] - 2024-12-19 + +- Drops support for Python 3.8 +- Adds support for Python 3.13 + +## [0.10.0] - 2024-10-31 + +- Adds an optional parameter to DataTable to disable rendering of string data as Rich Markup. +- Fixes a bug where None could be casted to a string and displayed as "None" ([tconbeer/harlequin#658](https://github.com/tconbeer/harlequin/issues/658)) + +## [0.9.0] - 2024-07-23 + +- Adds a PolarsBackend implementation of DataTableBackend. You must have `polars` installed to use the PolarsBackend. You can install it using the `polars` extra for this package. +- Fixes a crash from the ArrowBackend when attempting to instantiate negative datetimes after a timezone conversion. + +## [0.8.0] - 2024-07-10 + +- Fixes a crash when cell contents contained bad Rich Markdown ([tconbeer/harlequin#569](https://github.com/tconbeer/harlequin/issues/569)). +- Improves the appearance of data tooltips. + +## [0.7.1] - 2024-02-09 + +- Adds a `backend.source_data` property to exposue the underlying Arrow table, before slicing. + +## [0.7.0] - 2024-02-07 + +### Breaking Changes + +- Removes the NumpyBackend ([#78](https://github.com/tconbeer/textual-fastdatatable/issues/78)). + +### Features + +- Values are now formatted based on their type. Numbers have separators based on the locale, and numbers, dates/times/etc., and bools are right-aligned ([#70](https://github.com/tconbeer/textual-fastdatatable/issues/70)). + +### Bug Fixes + +- Fixes bug that caused either a crash or an empty table from initializing a table `from_records` or `from_pydict` with mixed (widening or narrowing) types in one column. + +## [0.6.3] - 2024-01-09 + +### Bug Fixes + +- Widens acceptable types for create_backend to accept a sequence of any iterable, not just iterables that are instances of typing.Iterable. + +## [0.6.2] - 2024-01-08 + +### Bug Fixes + +- Adds the tzdata package as a dependency for Windows installs, since Windows does not ship with a built-in tzdata database. + +## [0.6.1] - 2024-01-05 + +### Bug Fixes + +- Fixes the behavior of tab and shift+tab to cycle to the next/prev row if at the end/start of a row or table. +- Fixes a crash from pressing ctrl+c when the cursor type is column. + +## [0.6.0] - 2024-01-05 + +### Features + +- Adds keybindings for navigating the cursor in the data table. ctrl+right/left/up/down/home/end (with shift variants), tab, shift+tab, ctrl+a now all do roughly what they do in Excel (if the cursor type is `range`). + +## [0.5.1] - 2024-01-05 + +### Bug Fixes + +- Adds a dependency on pytz for Python <3.9 for timezone support. +- Fixes a bug where Arrow crashes while casting timestamptz to string ([tconbeer/harlequin#382](https://github.com/tconbeer/harlequin/issues/382)). + +### Performance + +- Vectorizes fallback string casting for datatypes unsupported by `pc.cast` ([#8](https://github.com/tconbeer/textual-fastdatatable/issues/8)) + +## [0.5.0] - 2023-12-21 + +### Features + +- Adds a `range` cursor type that will highlight a range of selected cells, like Excel. +- ctrl+c now posts a `SelectionCopied` message, with a values attribute that conttains a list of tuples of values from the data table. +- Adds a `max_column_content_width` parameter to DataTable. If set, DataTable will truncate values longer than the width, but show the full value in a tooltip on hover. + +## [0.4.1] - 2023-12-14 + +- Fixes a crash caused by calling `create_backend` with an empty sequence. + +## [0.4.0] - 2023-11-14 + +### Breaking API Changes + +- When calling `create_backend` with a sequence of iterables, the default behavior now assumes the data does not contain headers. You can restore the old behavior with `create_backend(has_headers=True)`. +- When calling `DataTable(data=...)` with a sequence of iterables, the first row is treated as a header only if `column_labels` is not provided. + +## [0.3.0] - 2023-11-11 + +### Features + +- The DataTable now accepts a `max_rows` kwarg; if provided, backends will only store the first `max_rows` and the DataTable will only present `max_rows`. The original row count of the data source is available as DataTable().source_row_count ([tconbeer/harlequin#281](https://github.com/tconbeer/harlequin/issues/281)). + +### API Changes + +- Backends must now accept a `max_rows` kwarg on initialization. + +## [0.2.1] - 2023-11-10 + +### Bug Fixes + +- Tables with the ArrowBackend no longer display incorrect output when column labels are duplicated ([#26](https://github.com/tconbeer/textual-fastdatatable/issues/26)). + +## [0.2.0] - 2023-11-08 + +### Features + +- Adds a `null_rep: str` argument when initializing the data table; this string will be used to replace missing data. +- Adds a `NumpyBackend` that uses Numpy Record Arrays; this backend is marginally slower than the `ArrowBackend` in most scenarios ([#23](https://github.com/tconbeer/textual-fastdatatable/issues/23)). + +### Bug Fixes + +- Fixes a crash when using `ArrowBackend.from_records(has_header=False)`. + +### Performance + +- Drastically improves performance for tables that are much wider than the viewport ([#12](https://github.com/tconbeer/textual-fastdatatable/issues/12)). + +### Benchmarks + +- Improves benchmarks to exclude data load times, disable garbage collection, and include more information about first paint and scroll performance. + +## [0.1.4] - 2023-11-06 + +- Fixes a crash when computing the widths of columns with no rows ([#19](https://github.com/tconbeer/textual-fastdatatable/issues/19)). + +## [0.1.3] - 2023-10-09 + +- Fixes a crash when creating a column from a null or complex type. + +## [0.1.2] - 2023-10-02 + +## [0.1.1] - 2023-09-29 + +- Fixes a crash when rows were added to an empty table. + +## [0.1.0] - 2023-09-29 + +- Initial release. Adds DataTable and ArrowBackend, which is 1000x faster for datasets of 500k records or more. + +[unreleased]: https://github.com/tconbeer/textual-fastdatatable/compare/0.12.0...HEAD +[0.12.0]: https://github.com/tconbeer/textual-fastdatatable/compare/0.11.0...0.12.0 +[0.11.0]: https://github.com/tconbeer/textual-fastdatatable/compare/0.10.0...0.11.0 +[0.10.0]: https://github.com/tconbeer/textual-fastdatatable/compare/0.9.0...0.10.0 +[0.9.0]: https://github.com/tconbeer/textual-fastdatatable/compare/0.8.0...0.9.0 +[0.8.0]: https://github.com/tconbeer/textual-fastdatatable/compare/0.7.1...0.8.0 +[0.7.1]: https://github.com/tconbeer/textual-fastdatatable/compare/0.7.0...0.7.1 +[0.7.0]: https://github.com/tconbeer/textual-fastdatatable/compare/0.6.3...0.7.0 +[0.6.3]: https://github.com/tconbeer/textual-fastdatatable/compare/0.6.2...0.6.3 +[0.6.2]: https://github.com/tconbeer/textual-fastdatatable/compare/0.6.1...0.6.2 +[0.6.1]: https://github.com/tconbeer/textual-fastdatatable/compare/0.6.0...0.6.1 +[0.6.0]: https://github.com/tconbeer/textual-fastdatatable/compare/0.5.1...0.6.0 +[0.5.1]: https://github.com/tconbeer/textual-fastdatatable/compare/0.5.0...0.5.1 +[0.5.0]: https://github.com/tconbeer/textual-fastdatatable/compare/0.4.1...0.5.0 +[0.4.1]: https://github.com/tconbeer/textual-fastdatatable/compare/0.4.0...0.4.1 +[0.4.0]: https://github.com/tconbeer/textual-fastdatatable/compare/0.3.0...0.4.0 +[0.3.0]: https://github.com/tconbeer/textual-fastdatatable/compare/0.2.1...0.3.0 +[0.2.1]: https://github.com/tconbeer/textual-fastdatatable/compare/0.2.0...0.2.1 +[0.2.0]: https://github.com/tconbeer/textual-fastdatatable/compare/0.1.4...0.2.0 +[0.1.4]: https://github.com/tconbeer/textual-fastdatatable/compare/0.1.3...0.1.4 +[0.1.3]: https://github.com/tconbeer/textual-fastdatatable/compare/0.1.2...0.1.3 +[0.1.2]: https://github.com/tconbeer/textual-fastdatatable/compare/0.1.1...0.1.2 +[0.1.1]: https://github.com/tconbeer/textual-fastdatatable/compare/0.1.0...0.1.1 +[0.1.0]: https://github.com/tconbeer/textual-fastdatatable/compare/4b9f99175d34f693dd0d4198c39d72f89caf6479...0.1.0 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..04ec0c8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Ted Conbeer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b8be4eb --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +.PHONY: check +check: + ruff format . + pytest + ruff check . --fix + mypy + +.PHONY: lint +lint: + ruff format . + ruff check . --fix + mypy + +.PHONY: serve +serve: + textual run --dev -c python -m textual_fastdatatable + +.PHONY: profile +profile: + pyinstrument -r html -o profile.html "src/scripts/run_arrow_wide.py" + +.PHONY: benchmark +benchmark: + python src/scripts/benchmark.py > /dev/null diff --git a/README.md b/README.md new file mode 100644 index 0000000..925f84f --- /dev/null +++ b/README.md @@ -0,0 +1,131 @@ +# textual-fastdatatable +A performance-focused reimplementation of Textual's DataTable widget, with a pluggable data storage backend. + +Textual's built-in DataTable widget is beautiful and powerful, but it can be slow to load large datasets. + +Here are some benchmarks on my relatively weak laptop. For each benchmark, we initialize a Textual App that +loads a dataset from a parquet file and mounts a data table; it then scrolls around the table +(10 pagedowns and 15 right arrows). + +For the built-in table and the others marked "from Records", the data is loaded into memory before the timer +is started; for the "Arrow from Parquet" back-end, the timer is started immediately. + +The times in each column represent the time to the first paint of the table, and the time after scrolling +is completed (we wait until the table is fully rendered after each scroll): + +Records | Built-In DataTable | FastDataTable (Arrow from Parquet) | FastDataTable (Arrow from Records) | FastDataTable (Numpy from Records) +--------|--------|--------|--------|-------- +lap_times_100.parquet | 0.019s / 1.716s | 0.012s / 1.724s | 0.011s / 1.700s | 0.011s / 1.688s +lap_times_1000.parquet | 0.103s / 1.931s | 0.011s / 1.859s | 0.011s / 1.799s | 0.015s / 1.848s +lap_times_10000.parquet | 0.977s / 2.824s | 0.013s / 1.834s | 0.016s / 1.812s | 0.078s / 1.869s +lap_times_100000.parquet | 11.773s / 13.770s | 0.025s / 1.790s | 0.156s / 1.824s | 0.567s / 2.347s +lap_times_538121.parquet | 62.960s / 65.760s | 0.077s / 1.803s | 0.379s / 2.234s | 3.324s / 5.031s +wide_10000.parquet | 5.110s / 10.539s | 0.024s / 3.373s | 0.042s / 3.278s | 0.369s / 3.461s +wide_100000.parquet | 51.144s / 56.604s | 0.054s / 3.294s | 0.429s / 3.642s | 3.628s / 6.732s + + +**NB:** FastDataTable currently does not support rows with a height of more than one line. See below for +more limitations, relative to the built-in DataTable. + +## Installation + +```bash +pip install textual-fastdatatable +``` + +## Usage + +If you already have data in Apache Arrow or another common table format: + +```py +from textual_fastdatatable import DataTable +data_table = DataTable(data = my_data) +``` + +The currently supported types are: + +```py +AutoBackendType = Union[ + pa.Table, + pa.RecordBatch, + Path, # to parquet only + str, # path to parquet only + Sequence[Iterable[Any]], + Mapping[str, Sequence[Any]], +] +``` + +To override the column labels and widths supplied by the backend: +```py +from textual_fastdatatable import DataTable +data_table = DataTable(data = my_data, column_labels=["Supports", "[red]Console[/]", "Markup!"], column_widths=[10, 5, None]) +``` + +You can also pass in a `backend` manually (if you want more control or want to plug in your own). + +```py +from textual_fastdatatable import ArrowBackend, DataTable, create_backend +backend = create_backend(my_data) +backend = ArrowBackend(my_arrow_table) +# from python dictionary in the form key: col_values +backend = ArrowBackend.from_pydict( + { + "col one": [1, 2, 3 ,4], + "col two": ["a", "b", "c", "d"], + } +) +# from a list of tuples or another sequence of iterables +backend = ArrowBackend.from_records( + [ + ("col one", "col two"), + (1, "a"), + (2, "b"), + (3, "c"), + (4, "d"), + ] +) +# from a path to a Parquet file: +backend = ArrowBackend.from_parquet("path/to/file.parquet") +``` + +## Limitations and Caveats + +The `DataTable` does not currently support rows with a height of more than one line. Only the first line of each row will be displayed. + +The `DataTable` does not currently support row labels. + +The `ArrowBackend` is optimized to be fast for large, immutable datasets. Mutating the data, +especially adding or removing rows, may be slow. + +The `ArrowBackend` cannot be initialized without data, however, the DataTable can (either with or without `column_labels`). + +The `ArrowBackend` cannot store arbitrary Python objects or Rich Renderables as values. It may widen types to strings unnecessarily. + +## Additional Features + +### Copying Data from the Table + +`ctrl+c` will post a SelectionCopied message with a list of tuples of the values selected by the cursor. To use, initialize with `cursor_type=range` from an app that does NOT inherit bindings. + +```py +from textual.app import App, ComposeResult + +from textual_fastdatatable import ArrowBackend, DataTable + + +class TableApp(App, inherit_bindings=False): + BINDINGS = [("ctrl+q", "quit", "Quit")] + + def compose(self) -> ComposeResult: + backend = ArrowBackend.from_parquet("./tests/data/lap_times_538121.parquet") + yield DataTable(backend=backend, cursor_type="range") + + +if __name__ == "__main__": + app = TableApp() + app.run() +``` + +### Truncating long values + +The `DataTable` will automatically calculate column widths; if you set a `max_column_content_width` at initialization, it will truncate any long values at that width; the full value will be visible on hover in a tooltip (and the full value will always be copied to the clipboard). diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..e93d8e0 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1769 @@ +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. + +[[package]] +name = "aiohappyeyeballs" +version = "2.4.4" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"}, + {file = "aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745"}, +] + +[[package]] +name = "aiohttp" +version = "3.11.9" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "aiohttp-3.11.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0411777249f25d11bd2964a230b3ffafcbed6cd65d0f2b132bc2b8f5b8c347c7"}, + {file = "aiohttp-3.11.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:499368eb904566fbdf1a3836a1532000ef1308f34a1bcbf36e6351904cced771"}, + {file = "aiohttp-3.11.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0b5a5009b0159a8f707879dc102b139466d8ec6db05103ec1520394fdd8ea02c"}, + {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:176f8bb8931da0613bb0ed16326d01330066bb1e172dd97e1e02b1c27383277b"}, + {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6435a66957cdba1a0b16f368bde03ce9c79c57306b39510da6ae5312a1a5b2c1"}, + {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:202f40fb686e5f93908eee0c75d1e6fbe50a43e9bd4909bf3bf4a56b560ca180"}, + {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39625703540feb50b6b7f938b3856d1f4886d2e585d88274e62b1bd273fae09b"}, + {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6beeac698671baa558e82fa160be9761cf0eb25861943f4689ecf9000f8ebd0"}, + {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:96726839a42429318017e67a42cca75d4f0d5248a809b3cc2e125445edd7d50d"}, + {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3f5461c77649358610fb9694e790956b4238ac5d9e697a17f63619c096469afe"}, + {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4313f3bc901255b22f01663eeeae167468264fdae0d32c25fc631d5d6e15b502"}, + {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d6e274661c74195708fc4380a4ef64298926c5a50bb10fbae3d01627d7a075b7"}, + {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db2914de2559809fdbcf3e48f41b17a493b58cb7988d3e211f6b63126c55fe82"}, + {file = "aiohttp-3.11.9-cp310-cp310-win32.whl", hash = "sha256:27935716f8d62c1c73010428db310fd10136002cfc6d52b0ba7bdfa752d26066"}, + {file = "aiohttp-3.11.9-cp310-cp310-win_amd64.whl", hash = "sha256:afbe85b50ade42ddff5669947afde9e8a610e64d2c80be046d67ec4368e555fa"}, + {file = "aiohttp-3.11.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:afcda759a69c6a8be3aae764ec6733155aa4a5ad9aad4f398b52ba4037942fe3"}, + {file = "aiohttp-3.11.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5bba6b83fde4ca233cfda04cbd4685ab88696b0c8eaf76f7148969eab5e248a"}, + {file = "aiohttp-3.11.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:442356e8924fe1a121f8c87866b0ecdc785757fd28924b17c20493961b3d6697"}, + {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f737fef6e117856400afee4f17774cdea392b28ecf058833f5eca368a18cf1bf"}, + {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea142255d4901b03f89cb6a94411ecec117786a76fc9ab043af8f51dd50b5313"}, + {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e1e9e447856e9b7b3d38e1316ae9a8c92e7536ef48373de758ea055edfd5db5"}, + {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7f6173302f8a329ca5d1ee592af9e628d3ade87816e9958dcf7cdae2841def7"}, + {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7c6147c6306f537cff59409609508a1d2eff81199f0302dd456bb9e7ea50c39"}, + {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e9d036a9a41fc78e8a3f10a86c2fc1098fca8fab8715ba9eb999ce4788d35df0"}, + {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2ac9fd83096df36728da8e2f4488ac3b5602238f602706606f3702f07a13a409"}, + {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d3108f0ad5c6b6d78eec5273219a5bbd884b4aacec17883ceefaac988850ce6e"}, + {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:96bbec47beb131bbf4bae05d8ef99ad9e5738f12717cfbbf16648b78b0232e87"}, + {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fc726c3fa8f606d07bd2b500e5dc4c0fd664c59be7788a16b9e34352c50b6b6b"}, + {file = "aiohttp-3.11.9-cp311-cp311-win32.whl", hash = "sha256:5720ebbc7a1b46c33a42d489d25d36c64c419f52159485e55589fbec648ea49a"}, + {file = "aiohttp-3.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:17af09d963fa1acd7e4c280e9354aeafd9e3d47eaa4a6bfbd2171ad7da49f0c5"}, + {file = "aiohttp-3.11.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c1f2d7fd583fc79c240094b3e7237d88493814d4b300d013a42726c35a734bc9"}, + {file = "aiohttp-3.11.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4b8a1b6c7a68c73191f2ebd3bf66f7ce02f9c374e309bdb68ba886bbbf1b938"}, + {file = "aiohttp-3.11.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd3f711f4c99da0091ced41dccdc1bcf8be0281dc314d6d9c6b6cf5df66f37a9"}, + {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44cb1a1326a0264480a789e6100dc3e07122eb8cd1ad6b784a3d47d13ed1d89c"}, + {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a7ddf981a0b953ade1c2379052d47ccda2f58ab678fca0671c7c7ca2f67aac2"}, + {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ffa45cc55b18d4ac1396d1ddb029f139b1d3480f1594130e62bceadf2e1a838"}, + {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cca505829cdab58c2495ff418c96092d225a1bbd486f79017f6de915580d3c44"}, + {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44d323aa80a867cb6db6bebb4bbec677c6478e38128847f2c6b0f70eae984d72"}, + {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b2fab23003c4bb2249729a7290a76c1dda38c438300fdf97d4e42bf78b19c810"}, + {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:be0c7c98e38a1e3ad7a6ff64af8b6d6db34bf5a41b1478e24c3c74d9e7f8ed42"}, + {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5cc5e0d069c56645446c45a4b5010d4b33ac6c5ebfd369a791b5f097e46a3c08"}, + {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9bcf97b971289be69638d8b1b616f7e557e1342debc7fc86cf89d3f08960e411"}, + {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c7333e7239415076d1418dbfb7fa4df48f3a5b00f8fdf854fca549080455bc14"}, + {file = "aiohttp-3.11.9-cp312-cp312-win32.whl", hash = "sha256:9384b07cfd3045b37b05ed002d1c255db02fb96506ad65f0f9b776b762a7572e"}, + {file = "aiohttp-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:f5252ba8b43906f206048fa569debf2cd0da0316e8d5b4d25abe53307f573941"}, + {file = "aiohttp-3.11.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:282e0a7ddd36ebc411f156aeaa0491e8fe7f030e2a95da532cf0c84b0b70bc66"}, + {file = "aiohttp-3.11.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ebd3e6b0c7d4954cca59d241970011f8d3327633d555051c430bd09ff49dc494"}, + {file = "aiohttp-3.11.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30f9f89ae625d412043f12ca3771b2ccec227cc93b93bb1f994db6e1af40a7d3"}, + {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a3b5b2c012d70c63d9d13c57ed1603709a4d9d7d473e4a9dfece0e4ea3d5f51"}, + {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ef1550bb5f55f71b97a6a395286db07f7f2c01c8890e613556df9a51da91e8d"}, + {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317251b9c9a2f1a9ff9cd093775b34c6861d1d7df9439ce3d32a88c275c995cd"}, + {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21cbe97839b009826a61b143d3ca4964c8590d7aed33d6118125e5b71691ca46"}, + {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:618b18c3a2360ac940a5503da14fa4f880c5b9bc315ec20a830357bcc62e6bae"}, + {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0cf4d814689e58f57ecd5d8c523e6538417ca2e72ff52c007c64065cef50fb2"}, + {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:15c4e489942d987d5dac0ba39e5772dcbed4cc9ae3710d1025d5ba95e4a5349c"}, + {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ec8df0ff5a911c6d21957a9182402aad7bf060eaeffd77c9ea1c16aecab5adbf"}, + {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ed95d66745f53e129e935ad726167d3a6cb18c5d33df3165974d54742c373868"}, + {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:647ec5bee7e4ec9f1034ab48173b5fa970d9a991e565549b965e93331f1328fe"}, + {file = "aiohttp-3.11.9-cp313-cp313-win32.whl", hash = "sha256:ef2c9499b7bd1e24e473dc1a85de55d72fd084eea3d8bdeec7ee0720decb54fa"}, + {file = "aiohttp-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:84de955314aa5e8d469b00b14d6d714b008087a0222b0f743e7ffac34ef56aff"}, + {file = "aiohttp-3.11.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e738aabff3586091221044b7a584865ddc4d6120346d12e28e788307cd731043"}, + {file = "aiohttp-3.11.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:28f29bce89c3b401a53d6fd4bee401ee943083bf2bdc12ef297c1d63155070b0"}, + {file = "aiohttp-3.11.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31de2f10f63f96cc19e04bd2df9549559beadd0b2ee2da24a17e7ed877ca8c60"}, + {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f31cebd8c27a36af6c7346055ac564946e562080ee1a838da724585c67474f"}, + {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0bcb7f6976dc0b6b56efde13294862adf68dd48854111b422a336fa729a82ea6"}, + {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8b13b9950d8b2f8f58b6e5842c4b842b5887e2c32e3f4644d6642f1659a530"}, + {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9c23e62f3545c2216100603614f9e019e41b9403c47dd85b8e7e5015bf1bde0"}, + {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec656680fc53a13f849c71afd0c84a55c536206d524cbc831cde80abbe80489e"}, + {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:36df00e0541f264ce42d62280281541a47474dfda500bc5b7f24f70a7f87be7a"}, + {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8dcfd14c712aa9dd18049280bfb2f95700ff6a8bde645e09f17c3ed3f05a0130"}, + {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14624d96f0d69cf451deed3173079a68c322279be6030208b045ab77e1e8d550"}, + {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4b01d9cfcb616eeb6d40f02e66bebfe7b06d9f2ef81641fdd50b8dd981166e0b"}, + {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:928f92f80e2e8d6567b87d3316c1fd9860ccfe36e87a9a7f5237d4cda8baa1ba"}, + {file = "aiohttp-3.11.9-cp39-cp39-win32.whl", hash = "sha256:c8a02f74ae419e3955af60f570d83187423e42e672a6433c5e292f1d23619269"}, + {file = "aiohttp-3.11.9-cp39-cp39-win_amd64.whl", hash = "sha256:0a97d657f6cf8782a830bb476c13f7d777cfcab8428ac49dde15c22babceb361"}, + {file = "aiohttp-3.11.9.tar.gz", hash = "sha256:a9266644064779840feec0e34f10a89b3ff1d2d6b751fe90017abcad1864fa7c"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.3.0" +aiosignal = ">=1.1.2" +async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] + +[[package]] +name = "aiohttp-jinja2" +version = "1.6" +description = "jinja2 template renderer for aiohttp.web (http server for asyncio)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohttp-jinja2-1.6.tar.gz", hash = "sha256:a3a7ff5264e5bca52e8ae547bbfd0761b72495230d438d05b6c0915be619b0e2"}, + {file = "aiohttp_jinja2-1.6-py3-none-any.whl", hash = "sha256:0df405ee6ad1b58e5a068a105407dc7dcc1704544c559f1938babde954f945c7"}, +] + +[package.dependencies] +aiohttp = ">=3.9.0" +jinja2 = ">=3.0.0" + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "async-timeout" +version = "5.0.1" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, +] + +[[package]] +name = "attrs" +version = "24.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, +] + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "distlib" +version = "0.3.9" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "filelock" +version = "3.16.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, + {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] +typing = ["typing-extensions (>=4.12.2)"] + +[[package]] +name = "frozenlist" +version = "1.5.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +files = [ + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5"}, + {file = "frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb"}, + {file = "frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf"}, + {file = "frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942"}, + {file = "frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f"}, + {file = "frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8"}, + {file = "frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03"}, + {file = "frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c"}, + {file = "frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e"}, + {file = "frozenlist-1.5.0-cp38-cp38-win32.whl", hash = "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723"}, + {file = "frozenlist-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c"}, + {file = "frozenlist-1.5.0-cp39-cp39-win32.whl", hash = "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3"}, + {file = "frozenlist-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0"}, + {file = "frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3"}, + {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"}, +] + +[[package]] +name = "identify" +version = "2.6.3" +description = "File identification library for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd"}, + {file = "identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "linkify-it-py" +version = "2.0.3" +description = "Links recognition library with FULL unicode support." +optional = false +python-versions = ">=3.7" +files = [ + {file = "linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048"}, + {file = "linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79"}, +] + +[package.dependencies] +uc-micro-py = "*" + +[package.extras] +benchmark = ["pytest", "pytest-benchmark"] +dev = ["black", "flake8", "isort", "pre-commit", "pyproject-flake8"] +doc = ["myst-parser", "sphinx", "sphinx-book-theme"] +test = ["coverage", "pytest", "pytest-cov"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +linkify-it-py = {version = ">=1,<3", optional = true, markers = "extra == \"linkify\""} +mdit-py-plugins = {version = "*", optional = true, markers = "extra == \"plugins\""} +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.4.2" +description = "Collection of plugins for markdown-it-py" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636"}, + {file = "mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5"}, +] + +[package.dependencies] +markdown-it-py = ">=1.0.0,<4.0.0" + +[package.extras] +code-style = ["pre-commit"] +rtd = ["myst-parser", "sphinx-book-theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "msgpack" +version = "1.1.0" +description = "MessagePack serializer" +optional = false +python-versions = ">=3.8" +files = [ + {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"}, + {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"}, + {file = "msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b"}, + {file = "msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044"}, + {file = "msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5"}, + {file = "msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88"}, + {file = "msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b"}, + {file = "msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b"}, + {file = "msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c"}, + {file = "msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc"}, + {file = "msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f"}, + {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec"}, + {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96"}, + {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870"}, + {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7"}, + {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb"}, + {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f"}, + {file = "msgpack-1.1.0-cp38-cp38-win32.whl", hash = "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b"}, + {file = "msgpack-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb"}, + {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1"}, + {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48"}, + {file = "msgpack-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c"}, + {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468"}, + {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74"}, + {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846"}, + {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346"}, + {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b"}, + {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8"}, + {file = "msgpack-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd"}, + {file = "msgpack-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325"}, + {file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"}, +] + +[[package]] +name = "multidict" +version = "6.1.0" +description = "multidict implementation" +optional = false +python-versions = ">=3.8" +files = [ + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"}, + {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"}, + {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"}, + {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"}, + {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, + {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, + {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"}, + {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"}, + {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd"}, + {file = "multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167"}, + {file = "multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43"}, + {file = "multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada"}, + {file = "multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a"}, + {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, + {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "mypy" +version = "1.13.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, + {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, + {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, + {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, + {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, + {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, + {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, + {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, + {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, + {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, + {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, + {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, + {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, + {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, + {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, + {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, + {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, + {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, + {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, + {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, + {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "numpy" +version = "1.26.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pandas" +version = "2.2.3" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, + {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"}, + {file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"}, + {file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"}, + {file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"}, + {file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761"}, + {file = "pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e"}, + {file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.7" + +[package.extras] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] + +[[package]] +name = "pandas-stubs" +version = "2.2.2.240807" +description = "Type annotations for pandas" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pandas_stubs-2.2.2.240807-py3-none-any.whl", hash = "sha256:893919ad82be4275f0d07bb47a95d08bae580d3fdea308a7acfcb3f02e76186e"}, + {file = "pandas_stubs-2.2.2.240807.tar.gz", hash = "sha256:64a559725a57a449f46225fbafc422520b7410bff9252b661a225b5559192a93"}, +] + +[package.dependencies] +numpy = ">=1.23.5" +types-pytz = ">=2022.1.1" + +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "polars" +version = "1.16.0" +description = "Blazingly fast DataFrame library" +optional = false +python-versions = ">=3.9" +files = [ + {file = "polars-1.16.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:072f5ff3b5fe05797c59890de0e464b34ede75a9735e7d7221622fa3a0616d8e"}, + {file = "polars-1.16.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ebaf7a1ea114b042fa9f1cd17d49436279eb30545dd74361a2f5e3febeb867cd"}, + {file = "polars-1.16.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e626d21dcd2566e1442dac414fe177bc70ebfc2f16620d59d778b1b774361018"}, + {file = "polars-1.16.0-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:53debcce55f68731ee2c7d6c787afdee26860ed6576f1ffa0cb9111b57f82857"}, + {file = "polars-1.16.0-cp39-abi3-win_amd64.whl", hash = "sha256:17efcb550c42d51034ff79702612b9184d8eac0d500de1dd7fb98490459276d3"}, + {file = "polars-1.16.0.tar.gz", hash = "sha256:dd99808b833872babe02434a809fd45c1cffe66a3d57123cdc5e447c7753d328"}, +] + +[package.extras] +adbc = ["adbc-driver-manager[dbapi]", "adbc-driver-sqlite[dbapi]"] +all = ["polars[async,cloudpickle,database,deltalake,excel,fsspec,graph,iceberg,numpy,pandas,plot,pyarrow,pydantic,style,timezone]"] +async = ["gevent"] +calamine = ["fastexcel (>=0.9)"] +cloudpickle = ["cloudpickle"] +connectorx = ["connectorx (>=0.3.2)"] +database = ["nest-asyncio", "polars[adbc,connectorx,sqlalchemy]"] +deltalake = ["deltalake (>=0.15.0)"] +excel = ["polars[calamine,openpyxl,xlsx2csv,xlsxwriter]"] +fsspec = ["fsspec"] +gpu = ["cudf-polars-cu12"] +graph = ["matplotlib"] +iceberg = ["pyiceberg (>=0.5.0)"] +numpy = ["numpy (>=1.16.0)"] +openpyxl = ["openpyxl (>=3.0.0)"] +pandas = ["pandas", "polars[pyarrow]"] +plot = ["altair (>=5.4.0)"] +pyarrow = ["pyarrow (>=7.0.0)"] +pydantic = ["pydantic"] +sqlalchemy = ["polars[pandas]", "sqlalchemy"] +style = ["great-tables (>=0.8.0)"] +timezone = ["backports-zoneinfo", "tzdata"] +xlsx2csv = ["xlsx2csv (>=0.8.0)"] +xlsxwriter = ["xlsxwriter"] + +[[package]] +name = "pre-commit" +version = "3.8.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, + {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "propcache" +version = "0.2.1" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.9" +files = [ + {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6"}, + {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2"}, + {file = "propcache-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b"}, + {file = "propcache-0.2.1-cp310-cp310-win32.whl", hash = "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4"}, + {file = "propcache-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e"}, + {file = "propcache-0.2.1-cp311-cp311-win32.whl", hash = "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034"}, + {file = "propcache-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518"}, + {file = "propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246"}, + {file = "propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30"}, + {file = "propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6"}, + {file = "propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1"}, + {file = "propcache-0.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6a9a8c34fb7bb609419a211e59da8887eeca40d300b5ea8e56af98f6fbbb1541"}, + {file = "propcache-0.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae1aa1cd222c6d205853b3013c69cd04515f9d6ab6de4b0603e2e1c33221303e"}, + {file = "propcache-0.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:accb6150ce61c9c4b7738d45550806aa2b71c7668c6942f17b0ac182b6142fd4"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eee736daafa7af6d0a2dc15cc75e05c64f37fc37bafef2e00d77c14171c2097"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7a31fc1e1bd362874863fdeed71aed92d348f5336fd84f2197ba40c59f061bd"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba4cfa1052819d16699e1d55d18c92b6e094d4517c41dd231a8b9f87b6fa681"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f089118d584e859c62b3da0892b88a83d611c2033ac410e929cb6754eec0ed16"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:781e65134efaf88feb447e8c97a51772aa75e48b794352f94cb7ea717dedda0d"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31f5af773530fd3c658b32b6bdc2d0838543de70eb9a2156c03e410f7b0d3aae"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:a7a078f5d37bee6690959c813977da5291b24286e7b962e62a94cec31aa5188b"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cea7daf9fc7ae6687cf1e2c049752f19f146fdc37c2cc376e7d0032cf4f25347"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8b3489ff1ed1e8315674d0775dc7d2195fb13ca17b3808721b54dbe9fd020faf"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9403db39be1393618dd80c746cb22ccda168efce239c73af13c3763ef56ffc04"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5d97151bc92d2b2578ff7ce779cdb9174337390a535953cbb9452fb65164c587"}, + {file = "propcache-0.2.1-cp39-cp39-win32.whl", hash = "sha256:9caac6b54914bdf41bcc91e7eb9147d331d29235a7c967c150ef5df6464fd1bb"}, + {file = "propcache-0.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:92fc4500fcb33899b05ba73276dfb684a20d31caa567b7cb5252d48f896a91b1"}, + {file = "propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54"}, + {file = "propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64"}, +] + +[[package]] +name = "pyarrow" +version = "18.1.0" +description = "Python library for Apache Arrow" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pyarrow-18.1.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e21488d5cfd3d8b500b3238a6c4b075efabc18f0f6d80b29239737ebd69caa6c"}, + {file = "pyarrow-18.1.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:b516dad76f258a702f7ca0250885fc93d1fa5ac13ad51258e39d402bd9e2e1e4"}, + {file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f443122c8e31f4c9199cb23dca29ab9427cef990f283f80fe15b8e124bcc49b"}, + {file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a03da7f2758645d17b7b4f83c8bffeae5bbb7f974523fe901f36288d2eab71"}, + {file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ba17845efe3aa358ec266cf9cc2800fa73038211fb27968bfa88acd09261a470"}, + {file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:3c35813c11a059056a22a3bef520461310f2f7eea5c8a11ef9de7062a23f8d56"}, + {file = "pyarrow-18.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9736ba3c85129d72aefa21b4f3bd715bc4190fe4426715abfff90481e7d00812"}, + {file = "pyarrow-18.1.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:eaeabf638408de2772ce3d7793b2668d4bb93807deed1725413b70e3156a7854"}, + {file = "pyarrow-18.1.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:3b2e2239339c538f3464308fd345113f886ad031ef8266c6f004d49769bb074c"}, + {file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f39a2e0ed32a0970e4e46c262753417a60c43a3246972cfc2d3eb85aedd01b21"}, + {file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31e9417ba9c42627574bdbfeada7217ad8a4cbbe45b9d6bdd4b62abbca4c6f6"}, + {file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:01c034b576ce0eef554f7c3d8c341714954be9b3f5d5bc7117006b85fcf302fe"}, + {file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f266a2c0fc31995a06ebd30bcfdb7f615d7278035ec5b1cd71c48d56daaf30b0"}, + {file = "pyarrow-18.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:d4f13eee18433f99adefaeb7e01d83b59f73360c231d4782d9ddfaf1c3fbde0a"}, + {file = "pyarrow-18.1.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:9f3a76670b263dc41d0ae877f09124ab96ce10e4e48f3e3e4257273cee61ad0d"}, + {file = "pyarrow-18.1.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:da31fbca07c435be88a0c321402c4e31a2ba61593ec7473630769de8346b54ee"}, + {file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:543ad8459bc438efc46d29a759e1079436290bd583141384c6f7a1068ed6f992"}, + {file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0743e503c55be0fdb5c08e7d44853da27f19dc854531c0570f9f394ec9671d54"}, + {file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d4b3d2a34780645bed6414e22dda55a92e0fcd1b8a637fba86800ad737057e33"}, + {file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c52f81aa6f6575058d8e2c782bf79d4f9fdc89887f16825ec3a66607a5dd8e30"}, + {file = "pyarrow-18.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ad4892617e1a6c7a551cfc827e072a633eaff758fa09f21c4ee548c30bcaf99"}, + {file = "pyarrow-18.1.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:84e314d22231357d473eabec709d0ba285fa706a72377f9cc8e1cb3c8013813b"}, + {file = "pyarrow-18.1.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:f591704ac05dfd0477bb8f8e0bd4b5dc52c1cadf50503858dce3a15db6e46ff2"}, + {file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acb7564204d3c40babf93a05624fc6a8ec1ab1def295c363afc40b0c9e66c191"}, + {file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74de649d1d2ccb778f7c3afff6085bd5092aed4c23df9feeb45dd6b16f3811aa"}, + {file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f96bd502cb11abb08efea6dab09c003305161cb6c9eafd432e35e76e7fa9b90c"}, + {file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:36ac22d7782554754a3b50201b607d553a8d71b78cdf03b33c1125be4b52397c"}, + {file = "pyarrow-18.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:25dbacab8c5952df0ca6ca0af28f50d45bd31c1ff6fcf79e2d120b4a65ee7181"}, + {file = "pyarrow-18.1.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6a276190309aba7bc9d5bd2933230458b3521a4317acfefe69a354f2fe59f2bc"}, + {file = "pyarrow-18.1.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ad514dbfcffe30124ce655d72771ae070f30bf850b48bc4d9d3b25993ee0e386"}, + {file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aebc13a11ed3032d8dd6e7171eb6e86d40d67a5639d96c35142bd568b9299324"}, + {file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6cf5c05f3cee251d80e98726b5c7cc9f21bab9e9783673bac58e6dfab57ecc8"}, + {file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:11b676cd410cf162d3f6a70b43fb9e1e40affbc542a1e9ed3681895f2962d3d9"}, + {file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:b76130d835261b38f14fc41fdfb39ad8d672afb84c447126b84d5472244cfaba"}, + {file = "pyarrow-18.1.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:0b331e477e40f07238adc7ba7469c36b908f07c89b95dd4bd3a0ec84a3d1e21e"}, + {file = "pyarrow-18.1.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:2c4dd0c9010a25ba03e198fe743b1cc03cd33c08190afff371749c52ccbbaf76"}, + {file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f97b31b4c4e21ff58c6f330235ff893cc81e23da081b1a4b1c982075e0ed4e9"}, + {file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a4813cb8ecf1809871fd2d64a8eff740a1bd3691bbe55f01a3cf6c5ec869754"}, + {file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:05a5636ec3eb5cc2a36c6edb534a38ef57b2ab127292a716d00eabb887835f1e"}, + {file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:73eeed32e724ea3568bb06161cad5fa7751e45bc2228e33dcb10c614044165c7"}, + {file = "pyarrow-18.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:a1880dd6772b685e803011a6b43a230c23b566859a6e0c9a276c1e0faf4f4052"}, + {file = "pyarrow-18.1.0.tar.gz", hash = "sha256:9386d3ca9c145b5539a1cfc75df07757dff870168c959b473a0bccbc3abc8c73"}, +] + +[package.extras] +test = ["cffi", "hypothesis", "pandas", "pytest", "pytz"] + +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyinstrument" +version = "5.0.0" +description = "Call stack profiler for Python. Shows you why your code is slow!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyinstrument-5.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6a83cf18f5594e1b1899b12b46df7aabca556eef895846ccdaaa3a46a37d1274"}, + {file = "pyinstrument-5.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1cc236313272d0222261be8e2b2a08e42d7ccbe54db9059babf4d77040da1880"}, + {file = "pyinstrument-5.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6dd685d68a31f3715ca61f82c37c1c2f8b75f45646bd9840e04681d91862bd85"}, + {file = "pyinstrument-5.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cecd0f6558f13fba74a9f036b2b168956206e9525dcb84c6add2d73ab61dc22"}, + {file = "pyinstrument-5.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40a8485c2e41082a20822001a6651667bb5327f6f5f6759987198593e45bb376"}, + {file = "pyinstrument-5.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a6294b7111348765ba4c311fc91821ed8b59c6690c4dab23aa7165a67da9e972"}, + {file = "pyinstrument-5.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a164f3dae5c7db2faa501639659d64034cde8db62a4d6744712593a369bc8629"}, + {file = "pyinstrument-5.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f6bac8a434407de6f2ebddbcdecdb19b324c9315cbb8b8c2352714f7ced8181"}, + {file = "pyinstrument-5.0.0-cp310-cp310-win32.whl", hash = "sha256:7e8dc887e535f5c5e5a2a64a0729496f11ddcef0c23b0a555d5ab6fa19759445"}, + {file = "pyinstrument-5.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c337190a1818841732643ba93065411591df526bc9de44b97ba8f56b581d2ef"}, + {file = "pyinstrument-5.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c9052f548ec5ccecc50676fbf1a1d0b60bdbd3cd67630c5253099af049d1f0ad"}, + {file = "pyinstrument-5.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:197d25487f52da3f8ec26d46db7202bc5d703cc73c1503371166417eb7cea14e"}, + {file = "pyinstrument-5.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a072d928dc16a32e0f3d1e51726f4472a69d66d838ee1d1bf248737fd70b9415"}, + {file = "pyinstrument-5.0.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2c7ae2c984879a645fce583bf3053b7e57495f60c1e158bb71ad7dfced1fbf1"}, + {file = "pyinstrument-5.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8284bf8847629c9a5054702b9306eab3ab14c2474959e01e606369ffbcf938bc"}, + {file = "pyinstrument-5.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4fd94cc725efb1dd41ae8e20a5f06a6a5363dec959e8a9dacbac3f4d12d28f03"}, + {file = "pyinstrument-5.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e0fdb9fe6f9c694940410dcc82e23a3fe2928114328efd35047fc0bb8a6c959f"}, + {file = "pyinstrument-5.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9ffe938e63173ceb8ce7b6b309ce26c9d44d16f53c0162d89d6e706eb9e69802"}, + {file = "pyinstrument-5.0.0-cp311-cp311-win32.whl", hash = "sha256:80d2a248516f372a89e0fe9ddf4a9d6388a4c6481b6ebd3dfe01b3cd028c0275"}, + {file = "pyinstrument-5.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:7ccf4267aff62de0e1d976e8f5da25dcb69737ae86e38d3cfffa24877837e7d1"}, + {file = "pyinstrument-5.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:dec3529a5351ea160baeef1ef2a6e28b1a7a7b3fb5e9863fae8de6da73d0f69a"}, + {file = "pyinstrument-5.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5a39e3ef84c56183f8274dfd584b8c2fae4783c6204f880513e70ab2440b9137"}, + {file = "pyinstrument-5.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3938f063ee065e05826628dadf1fb32c7d26b22df4a945c22f7fe25ea1ba6a2"}, + {file = "pyinstrument-5.0.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f18990cc16b2e23b54738aa2f222863e1d36daaaec8f67b1613ddfa41f5b24db"}, + {file = "pyinstrument-5.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3731412b5bfdcef8014518f145140c69384793e218863a33a39ccfe5fb42045"}, + {file = "pyinstrument-5.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02b2eaf38460b14eea646d6bb7f373eb5bb5691d13f788e80bdcb3a4eaa2519e"}, + {file = "pyinstrument-5.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e57db06590f13657b2bce8c4d9cf8e9e2bd90bb729bcbbe421c531ba67ad7add"}, + {file = "pyinstrument-5.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddaa3001c1b798ec9bf1266ef476bbc0834b74d547d531f5ed99e7d05ac5d81b"}, + {file = "pyinstrument-5.0.0-cp312-cp312-win32.whl", hash = "sha256:b69ff982acf5ef2f4e0f32ce9b4b598f256faf88438f233ea3a72f1042707e5b"}, + {file = "pyinstrument-5.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:0bf4ef061d60befe72366ce0ed4c75dee5be089644de38f9936d2df0bcf44af0"}, + {file = "pyinstrument-5.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79a54def2d4aa83a4ed37c6cffc5494ae5de140f0453169eb4f7c744cc249d3a"}, + {file = "pyinstrument-5.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9538f746f166a40c8802ebe5c3e905d50f3faa189869cd71c083b8a639e574bb"}, + {file = "pyinstrument-5.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bbab65cae1483ad8a18429511d1eac9e3efec9f7961f2fd1bf90e1e2d69ef15"}, + {file = "pyinstrument-5.0.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4351ad041d208c597e296a0e9c2e6e21cc96804608bcafa40cfa168f3c2b8f79"}, + {file = "pyinstrument-5.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ceee5252f4580abec29bcc5c965453c217b0d387c412a5ffb8afdcda4e648feb"}, + {file = "pyinstrument-5.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b3050a4e7033103a13cfff9802680e2070a9173e1a258fa3f15a80b4eb9ee278"}, + {file = "pyinstrument-5.0.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3b1f44a34da7810938df615fb7cbc43cd879b42ca6b5cd72e655aee92149d012"}, + {file = "pyinstrument-5.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fde075196c8a3b2be191b8da05b92ff909c78d308f82df56d01a8cfdd6da07b9"}, + {file = "pyinstrument-5.0.0-cp313-cp313-win32.whl", hash = "sha256:1a9b62a8b54e05e7723eb8b9595fadc43559b73290c87b3b1cb2dc5944559790"}, + {file = "pyinstrument-5.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2478d2c55f77ad8e281e67b0dfe7c2176304bb824c307e86e11890f5e68d7feb"}, + {file = "pyinstrument-5.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c2e3b4283f85232fd5818e2153e6798bceb39a8c3ccfaa22fae08faf554740b7"}, + {file = "pyinstrument-5.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fb1139d2822abff1cbf1c81c018341f573b7afa23a94ce74888a0f6f47828cbc"}, + {file = "pyinstrument-5.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c971566d86ba46a7233d3f5b0d85d7ee4c9863f541f5d8f796c3947ebe17f68"}, + {file = "pyinstrument-5.0.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:429376235960179d6ab9b97e7871090059d39de160b4e3b2723672f30e8eea8e"}, + {file = "pyinstrument-5.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8599b4b0630c776b30fc3c4f7476d5e3814ee7fe42d99131644fe3c00b40fdf1"}, + {file = "pyinstrument-5.0.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a8bc688afa2a5368042a7cb56866d5a28fdff8f37a282f7be79b17cae042841b"}, + {file = "pyinstrument-5.0.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5d34c06e2276d1f549a540bccb063688ea3d876e6df7c391205f1c8b4b96d5c8"}, + {file = "pyinstrument-5.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4d3b2ec6e028731dbb2ba8cf06f19030162789e6696bca990a09519881ad42fb"}, + {file = "pyinstrument-5.0.0-cp38-cp38-win32.whl", hash = "sha256:5ed6f5873a7526ec5915e45d956d044334ef302653cf63649e48c41561aaa285"}, + {file = "pyinstrument-5.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:9e87d65bae7d0f5ef50908e35d67d43b7cc566909995cc99e91721bb49b4ea06"}, + {file = "pyinstrument-5.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bd953163616bc29c2ccb1e4c0e48ccdd11e0a97fc849da26bc362bba372019ba"}, + {file = "pyinstrument-5.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8d2a7279ed9b6d7cdae247bc2e57095a32f35dfe32182c334ab0ac3eb02e0eac"}, + {file = "pyinstrument-5.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68001dfcb8a37b624a1c3de5d2ee7d634f63eac7a6dd1357b7370a5cdbdcf567"}, + {file = "pyinstrument-5.0.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5c4c3cc6410ad5afe0e352a7fb09fb1ab85eb5676ec5ec8522123759d9cc68f"}, + {file = "pyinstrument-5.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d87ddab66b1b3525ad3abc49a88aaa51efcaf83578e9d2a702c03a1cea39f28"}, + {file = "pyinstrument-5.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03182ffaa9c91687cbaba80dc0c5a47015c5ea170fe642f632d88e885cf07356"}, + {file = "pyinstrument-5.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:39b60417c9c12eed04e1886644e92aa0b281d72e5d0b097b16253cade43110f7"}, + {file = "pyinstrument-5.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7bb389b6d1573361bd1367b296133c5c69184e35fc18db22e29e8cdf56f158f9"}, + {file = "pyinstrument-5.0.0-cp39-cp39-win32.whl", hash = "sha256:ae69478815edb3c63e7ebf82e1e13e38c3fb2bab833b1c013643c3475b1b8cf5"}, + {file = "pyinstrument-5.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:83caeb4150c0334e9e290c0f9bb164ff6bdc199065ecb62016268e8a88589a51"}, + {file = "pyinstrument-5.0.0.tar.gz", hash = "sha256:144f98eb3086667ece461f66324bf1cc1ee0475b399ab3f9ded8449cc76b7c90"}, +] + +[package.extras] +bin = ["click", "nox"] +docs = ["furo (==2024.7.18)", "myst-parser (==3.0.1)", "sphinx (==7.4.7)", "sphinx-autobuild (==2024.4.16)", "sphinxcontrib-programoutput (==0.17)"] +examples = ["django", "litestar", "numpy"] +test = ["cffi (>=1.17.0)", "flaky", "greenlet (>=3)", "ipython", "pytest", "pytest-asyncio (==0.23.8)", "trio"] +types = ["typing-extensions"] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.23.8" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, + {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-textual-snapshot" +version = "0.4.0" +description = "Snapshot testing for Textual apps" +optional = false +python-versions = ">=3.6,<4.0" +files = [ + {file = "pytest_textual_snapshot-0.4.0-py3-none-any.whl", hash = "sha256:879cc5de29cdd31cfe1b6daeb1dc5e42682abebcf4f88e7e3375bd5200683fc0"}, + {file = "pytest_textual_snapshot-0.4.0.tar.gz", hash = "sha256:63782e053928a925d88ff7359dd640f2900e23bc708b3007f8b388e65f2527cb"}, +] + +[package.dependencies] +jinja2 = ">=3.0.0" +pytest = ">=7.0.0" +rich = ">=12.0.0" +syrupy = ">=3.0.0" +textual = ">=0.28.0" + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2024.2" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, + {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "rich" +version = "13.9.4" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "ruff" +version = "0.5.7" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"}, + {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"}, + {file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"}, + {file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"}, + {file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"}, + {file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"}, + {file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"}, +] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "syrupy" +version = "4.8.0" +description = "Pytest Snapshot Test Utility" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "syrupy-4.8.0-py3-none-any.whl", hash = "sha256:544f4ec6306f4b1c460fdab48fd60b2c7fe54a6c0a8243aeea15f9ad9c638c3f"}, + {file = "syrupy-4.8.0.tar.gz", hash = "sha256:648f0e9303aaa8387c8365d7314784c09a6bab0a407455c6a01d6a4f5c6a8ede"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9.0.0" + +[[package]] +name = "textual" +version = "0.89.1" +description = "Modern Text User Interface framework" +optional = false +python-versions = "<4.0.0,>=3.8.1" +files = [ + {file = "textual-0.89.1-py3-none-any.whl", hash = "sha256:0a5d214df6e951b4a2c421e13d0b608482882471c1e34ea74a3631adede8054f"}, + {file = "textual-0.89.1.tar.gz", hash = "sha256:66befe80e2bca5a8c876cd8ceeaf01752267b6b1dc1d0f73071f1f1e15d90cc8"}, +] + +[package.dependencies] +markdown-it-py = {version = ">=2.1.0", extras = ["linkify", "plugins"]} +platformdirs = ">=3.6.0,<5" +rich = ">=13.3.3" +typing-extensions = ">=4.4.0,<5.0.0" + +[package.extras] +syntax = ["tree-sitter (>=0.23.0)", "tree-sitter-bash (>=0.23.0)", "tree-sitter-css (>=0.23.0)", "tree-sitter-go (>=0.23.0)", "tree-sitter-html (>=0.23.0)", "tree-sitter-java (>=0.23.0)", "tree-sitter-javascript (>=0.23.0)", "tree-sitter-json (>=0.24.0)", "tree-sitter-markdown (>=0.3.0)", "tree-sitter-python (>=0.23.0)", "tree-sitter-regex (>=0.24.0)", "tree-sitter-rust (>=0.23.0)", "tree-sitter-sql (>=0.3.0)", "tree-sitter-toml (>=0.6.0)", "tree-sitter-xml (>=0.7.0)", "tree-sitter-yaml (>=0.6.0)"] + +[[package]] +name = "textual-dev" +version = "1.7.0" +description = "Development tools for working with Textual" +optional = false +python-versions = "<4.0.0,>=3.8.1" +files = [ + {file = "textual_dev-1.7.0-py3-none-any.whl", hash = "sha256:a93a846aeb6a06edb7808504d9c301565f7f4bf2e7046d56583ed755af356c8d"}, + {file = "textual_dev-1.7.0.tar.gz", hash = "sha256:bf1a50eaaff4cd6a863535dd53f06dbbd62617c371604f66f56de3908220ccd5"}, +] + +[package.dependencies] +aiohttp = ">=3.8.1" +click = ">=8.1.2" +msgpack = ">=1.0.3" +textual = ">=0.86.2" +textual_serve = ">=1.0.3" +typing-extensions = ">=4.4.0,<5.0.0" + +[[package]] +name = "textual-serve" +version = "1.1.1" +description = "Turn your Textual TUIs in to web applications" +optional = false +python-versions = ">=3.8" +files = [ + {file = "textual_serve-1.1.1-py3-none-any.whl", hash = "sha256:568782f1c0e60e3f7039d9121e1cb5c2f4ca1aaf6d6bd7aeb833d5763a534cb2"}, + {file = "textual_serve-1.1.1.tar.gz", hash = "sha256:71c662472c462e5e368defc660ee6e8eae3bfda88ca40c050c55474686eb0c54"}, +] + +[package.dependencies] +aiohttp = ">=3.9.5" +aiohttp-jinja2 = ">=1.6" +jinja2 = ">=3.1.4" +rich = "*" +textual = ">=0.66.0" + +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "types-pytz" +version = "2024.2.0.20241003" +description = "Typing stubs for pytz" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-pytz-2024.2.0.20241003.tar.gz", hash = "sha256:575dc38f385a922a212bac00a7d6d2e16e141132a3c955078f4a4fd13ed6cb44"}, + {file = "types_pytz-2024.2.0.20241003-py3-none-any.whl", hash = "sha256:3e22df1336c0c6ad1d29163c8fda82736909eb977281cb823c57f8bae07118b7"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "tzdata" +version = "2024.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, +] + +[[package]] +name = "uc-micro-py" +version = "1.0.3" +description = "Micro subset of unicode data files for linkify-it-py projects." +optional = false +python-versions = ">=3.7" +files = [ + {file = "uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a"}, + {file = "uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5"}, +] + +[package.extras] +test = ["coverage", "pytest", "pytest-cov"] + +[[package]] +name = "virtualenv" +version = "20.28.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +files = [ + {file = "virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0"}, + {file = "virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "yarl" +version = "1.18.3" +description = "Yet another URL library" +optional = false +python-versions = ">=3.9" +files = [ + {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"}, + {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"}, + {file = "yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690"}, + {file = "yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6"}, + {file = "yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a"}, + {file = "yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1"}, + {file = "yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285"}, + {file = "yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2"}, + {file = "yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8"}, + {file = "yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d"}, + {file = "yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c"}, + {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04"}, + {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719"}, + {file = "yarl-1.18.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1"}, + {file = "yarl-1.18.3-cp39-cp39-win32.whl", hash = "sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5"}, + {file = "yarl-1.18.3-cp39-cp39-win_amd64.whl", hash = "sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9"}, + {file = "yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b"}, + {file = "yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.0" + +[extras] +polars = ["polars"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.9,<3.14" +content-hash = "d7644efc2e218d280ff6b6678c36e7b2b23df09a420bf14d886ef37cc5505fed" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b95600c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,81 @@ +[tool.poetry] +name = "textual-fastdatatable" +version = "0.12.0" +description = "A performance-focused reimplementation of Textual's DataTable widget, with a pluggable data storage backend." +authors = ["Ted Conbeer "] +license = "MIT" +readme = "README.md" +packages = [ + { include = "textual_fastdatatable", from = "src" }, +] + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + + +[tool.poetry.dependencies] +python = ">=3.9,<3.14" +textual = ">=0.89.1" +pyarrow = ">=16.1.0" +polars = { version = ">=0.20.0", optional = true } +tzdata = { version = ">=2023", markers = "sys_platform == 'win32'" } # arrow timestamptz support on windows + +[tool.poetry.extras] +polars = ["polars"] + +[tool.poetry.group.dev.dependencies] +pre-commit = "^3.3.1" +textual-dev = "^1.0.1" +pandas = "^2.1.1" +numpy = "^1" +pyinstrument = "^5" + +[tool.poetry.group.static.dependencies] +ruff = "^0.5" +mypy = "^1.10.0" +pandas-stubs = "^2.1.1" + +[tool.poetry.group.test.dependencies] +pytest = "^7.3.1" +pytest-asyncio = ">=0.21,<0.24" +pytest-textual-snapshot = ">=0.4.0" +polars = ">=0.20.0" + + +[tool.ruff] +target-version = "py39" + +[tool.ruff.lint] +select = ["A", "B", "E", "F", "I"] + +[tool.mypy] +python_version = "3.9" +files = [ + "src/**/*.py", + "tests/unit_tests/**/*.py" +] +mypy_path = "src:stubs" + +show_column_numbers = true + +# show error messages from unrelated files +follow_imports = "normal" + +# be strict +disallow_untyped_calls = true +disallow_untyped_defs = true +check_untyped_defs = true +disallow_untyped_decorators = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +strict_optional = true + +warn_return_any = true +warn_no_return = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_unused_configs = true + +no_implicit_reexport = true +strict_equality = true diff --git a/src/scripts/benchmark.py b/src/scripts/benchmark.py new file mode 100644 index 0000000..014513e --- /dev/null +++ b/src/scripts/benchmark.py @@ -0,0 +1,157 @@ +from __future__ import annotations + +import gc +from pathlib import Path +from time import perf_counter + +import pandas as pd +import polars as pl +from textual.app import App, ComposeResult +from textual.driver import Driver +from textual.pilot import Pilot +from textual.types import CSSPathType +from textual.widgets import DataTable as BuiltinDataTable +from textual_fastdatatable import ArrowBackend +from textual_fastdatatable import DataTable as FastDataTable +from textual_fastdatatable.backend import PolarsBackend + +BENCHMARK_DATA = Path(__file__).parent.parent.parent / "tests" / "data" + + +async def scroller(pilot: Pilot) -> None: + first_paint = perf_counter() - pilot.app.start # type: ignore + for _ in range(5): + await pilot.press("pagedown") + for _ in range(15): + await pilot.press("right") + for _ in range(5): + await pilot.press("pagedown") + elapsed = perf_counter() - pilot.app.start # type: ignore + pilot.app.exit(result=(first_paint, elapsed)) + + +class BuiltinApp(App): + TITLE = "Built-In DataTable" + + def __init__( + self, + data_path: Path, + driver_class: type[Driver] | None = None, + css_path: CSSPathType | None = None, + watch_css: bool = False, + ): + super().__init__(driver_class, css_path, watch_css) + self.data_path = data_path + + def compose(self) -> ComposeResult: + df = pd.read_parquet(self.data_path) + rows = [tuple(row) for row in df.itertuples(index=False)] + self.start = perf_counter() + table: BuiltinDataTable = BuiltinDataTable() + table.add_columns(*[str(col) for col in df.columns]) + for row in rows: + table.add_row(*row, height=1, label=None) + yield table + + +class ArrowBackendApp(App): + TITLE = "FastDataTable (Arrow from Parquet)" + + def __init__( + self, + data_path: Path, + driver_class: type[Driver] | None = None, + css_path: CSSPathType | None = None, + watch_css: bool = False, + ): + super().__init__(driver_class, css_path, watch_css) + self.data_path = data_path + + def compose(self) -> ComposeResult: + self.start = perf_counter() + yield FastDataTable(data=self.data_path) + + +class ArrowBackendAppFromRecords(App): + TITLE = "FastDataTable (Arrow from Records)" + + def __init__( + self, + data_path: Path, + driver_class: type[Driver] | None = None, + css_path: CSSPathType | None = None, + watch_css: bool = False, + ): + super().__init__(driver_class, css_path, watch_css) + self.data_path = data_path + + def compose(self) -> ComposeResult: + df = pd.read_parquet(self.data_path) + rows = [tuple(row) for row in df.itertuples(index=False)] + self.start = perf_counter() + backend = ArrowBackend.from_records(rows, has_header=False) + table = FastDataTable( + backend=backend, column_labels=[str(col) for col in df.columns] + ) + yield table + + +class PolarsBackendApp(App): + TITLE = "FastDataTable (Polars from Parquet)" + + def __init__( + self, + data_path: Path, + driver_class: type[Driver] | None = None, + css_path: CSSPathType | None = None, + watch_css: bool = False, + ): + super().__init__(driver_class, css_path, watch_css) + self.data_path = data_path + + def compose(self) -> ComposeResult: + self.start = perf_counter() + yield FastDataTable( + data=PolarsBackend.from_dataframe(pl.read_parquet(self.data_path)) + ) + + +if __name__ == "__main__": + app_defs = [ + BuiltinApp, + ArrowBackendApp, + ArrowBackendAppFromRecords, + PolarsBackendApp, + ] + bench = [ + (f"lap_times_{n}.parquet", 3 if n <= 10000 else 1) + for n in [100, 1000, 10000, 100000, 538121] + ] + bench.extend([(f"wide_{n}.parquet", 1) for n in [10000, 100000]]) + with open("results.md", "w") as f: + print( + "Records |", + " | ".join([a.TITLE for a in app_defs]), # type: ignore + sep="", + file=f, + ) + print("--------|", "|".join(["--------" for _ in app_defs]), sep="", file=f) + for p, tries in bench: + first_paint: list[list[float]] = [list() for _ in app_defs] + elapsed: list[list[float]] = [list() for _ in app_defs] + for i, app_cls in enumerate(app_defs): + for _ in range(tries): + app = app_cls(BENCHMARK_DATA / p) + gc.disable() + fp, el = app.run(headless=True, auto_pilot=scroller) # type: ignore + gc.collect() + first_paint[i].append(fp) + elapsed[i].append(el) + gc.enable() + avg_first_paint = [sum(app_times) / tries for app_times in first_paint] + avg_elapsed = [sum(app_times) / tries for app_times in elapsed] + formatted = [ + f"{fp:7,.3f}s / {el:7,.3f}s" + for fp, el in zip(avg_first_paint, avg_elapsed) + ] + print(f"{p} | {' | '.join(formatted)}", file=f) diff --git a/src/scripts/run_arrow_wide.py b/src/scripts/run_arrow_wide.py new file mode 100644 index 0000000..2938fcc --- /dev/null +++ b/src/scripts/run_arrow_wide.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from pathlib import Path + +from textual.app import App, ComposeResult +from textual.driver import Driver +from textual.types import CSSPathType +from textual_fastdatatable import DataTable + +BENCHMARK_DATA = Path(__file__).parent.parent.parent / "tests" / "data" + + +class ArrowBackendApp(App): + TITLE = "FastDataTable (Arrow)" + + def __init__( + self, + data_path: Path, + driver_class: type[Driver] | None = None, + css_path: CSSPathType | None = None, + watch_css: bool = False, + ): + super().__init__(driver_class, css_path, watch_css) + self.data_path = data_path + + def compose(self) -> ComposeResult: + yield DataTable(data=self.data_path) + + +if __name__ == "__main__": + app = ArrowBackendApp(data_path=BENCHMARK_DATA / "wide_100000.parquet") + app.run() diff --git a/src/scripts/run_builtin_wide.py b/src/scripts/run_builtin_wide.py new file mode 100644 index 0000000..2e5311f --- /dev/null +++ b/src/scripts/run_builtin_wide.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from pathlib import Path + +import pandas as pd +from textual.app import App, ComposeResult +from textual.driver import Driver +from textual.types import CSSPathType +from textual.widgets import DataTable + +BENCHMARK_DATA = Path(__file__).parent.parent.parent / "tests" / "data" + + +class BuiltinApp(App): + TITLE = "Built-In DataTable" + + def __init__( + self, + data_path: Path, + driver_class: type[Driver] | None = None, + css_path: CSSPathType | None = None, + watch_css: bool = False, + ): + super().__init__(driver_class, css_path, watch_css) + self.data_path = data_path + + def compose(self) -> ComposeResult: + df = pd.read_parquet(self.data_path) + rows = [tuple(row) for row in df.itertuples(index=False)] + table: DataTable = DataTable() + table.add_columns(*[str(col) for col in df.columns]) + for row in rows: + table.add_row(*row, height=1, label=None) + yield table + + +if __name__ == "__main__": + app = BuiltinApp(data_path=BENCHMARK_DATA / "wide_10000.parquet") + app.run() diff --git a/src/textual_fastdatatable/__init__.py b/src/textual_fastdatatable/__init__.py new file mode 100644 index 0000000..44dfe00 --- /dev/null +++ b/src/textual_fastdatatable/__init__.py @@ -0,0 +1,13 @@ +from textual_fastdatatable.backend import ( + ArrowBackend, + DataTableBackend, + create_backend, +) +from textual_fastdatatable.data_table import DataTable + +__all__ = [ + "DataTable", + "ArrowBackend", + "DataTableBackend", + "create_backend", +] diff --git a/src/textual_fastdatatable/__main__.py b/src/textual_fastdatatable/__main__.py new file mode 100644 index 0000000..c3f6788 --- /dev/null +++ b/src/textual_fastdatatable/__main__.py @@ -0,0 +1,19 @@ +from textual.app import App, ComposeResult + +from textual_fastdatatable import ArrowBackend, DataTable + + +class TableApp(App, inherit_bindings=False): + BINDINGS = [("ctrl+q", "quit", "Quit"), ("ctrl+d", "quit", "Quit")] + + def compose(self) -> ComposeResult: + backend = ArrowBackend.from_parquet("./tests/data/wide_100000.parquet") + yield DataTable(backend=backend, cursor_type="range", fixed_columns=2) + + +if __name__ == "__main__": + import locale + + locale.setlocale(locale.LC_ALL, "") + app = TableApp() + app.run() diff --git a/src/textual_fastdatatable/backend.py b/src/textual_fastdatatable/backend.py new file mode 100644 index 0000000..ed667f1 --- /dev/null +++ b/src/textual_fastdatatable/backend.py @@ -0,0 +1,706 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from contextlib import suppress +from datetime import date, datetime +from pathlib import Path +from typing import ( + Any, + Dict, + Generic, + Iterable, + Literal, + Mapping, + Sequence, + TypeVar, +) + +import pyarrow as pa +import pyarrow.compute as pc +import pyarrow.lib as pal +import pyarrow.parquet as pq +import pyarrow.types as pt +from rich.console import Console + +from textual_fastdatatable.formatter import measure_width + +AutoBackendType = Any + +try: + import polars as pl + import polars.datatypes as pld +except ImportError: + _HAS_POLARS = False +else: + _HAS_POLARS = True + + +def create_backend( + data: "AutoBackendType", + max_rows: int | None = None, + has_header: bool = False, +) -> DataTableBackend: + if isinstance(data, pa.Table): + return ArrowBackend(data, max_rows=max_rows) + if isinstance(data, pa.RecordBatch): + return ArrowBackend.from_batches(data, max_rows=max_rows) + if _HAS_POLARS and isinstance(data, pl.DataFrame): + return PolarsBackend.from_dataframe(data, max_rows=max_rows) + + if isinstance(data, Path) or isinstance(data, str): + data = Path(data) + if data.suffix in [".pqt", ".parquet"]: + return ArrowBackend.from_parquet(data, max_rows=max_rows) + if _HAS_POLARS: + return PolarsBackend.from_file_path( + data, max_rows=max_rows, has_header=has_header + ) + if isinstance(data, Sequence) and not data: + return ArrowBackend(pa.table([]), max_rows=max_rows) + if isinstance(data, Sequence) and _is_iterable(data[0]): + return ArrowBackend.from_records(data, max_rows=max_rows, has_header=has_header) + + if ( + isinstance(data, Mapping) + and isinstance(next(iter(data.keys())), str) + and isinstance(next(iter(data.values())), Sequence) + ): + return ArrowBackend.from_pydict(data, max_rows=max_rows) + + raise TypeError( + f"Cannot automatically create backend for data of type: {type(data)}. " + f"Data must be of type: Union[pa.Table, pa.RecordBatch, Path, str, " + "Sequence[Iterable[Any]], Mapping[str, Sequence[Any]], pl.DataFrame", + ) + + +def _is_iterable(item: Any) -> bool: + try: + iter(item) + except TypeError: + return False + else: + return True + + +_TableTypeT = TypeVar("_TableTypeT") + + +class DataTableBackend(ABC, Generic[_TableTypeT]): + data: _TableTypeT + + @abstractmethod + def __init__(self, data: _TableTypeT, max_rows: int | None = None) -> None: + pass + + @classmethod + @abstractmethod + def from_pydict( + cls, data: Mapping[str, Sequence[Any]], max_rows: int | None = None + ) -> "DataTableBackend": + pass + + @property + @abstractmethod + def source_data(self) -> _TableTypeT: + """ + Return the source data as an Arrow table + """ + pass + + @property + @abstractmethod + def source_row_count(self) -> int: + """ + The number of rows in the source data, before filtering down to max_rows + """ + pass + + @property + @abstractmethod + def row_count(self) -> int: + """ + The number of rows in backend's retained data, after filtering down to max_rows + """ + pass + + @property + def column_count(self) -> int: + return len(self.columns) + + @property + @abstractmethod + def columns(self) -> Sequence[str]: + """ + A list of column labels + """ + pass + + @property + @abstractmethod + def column_content_widths(self) -> Sequence[int]: + """ + A list of integers corresponding to the widest utf8 string length + of any data in each column. + """ + pass + + @abstractmethod + def get_row_at(self, index: int) -> Sequence[Any]: + pass + + @abstractmethod + def get_column_at(self, index: int) -> Sequence[Any]: + pass + + @abstractmethod + def get_cell_at(self, row_index: int, column_index: int) -> Any: + pass + + @abstractmethod + def append_column(self, label: str, default: Any | None = None) -> int: + """ + Returns column index + """ + + @abstractmethod + def append_rows(self, records: Iterable[Iterable[Any]]) -> list[int]: + """ + Returns new row indicies + """ + pass + + @abstractmethod + def drop_row(self, row_index: int) -> None: + pass + + @abstractmethod + def update_cell(self, row_index: int, column_index: int, value: Any) -> None: + """ + Raises IndexError if bad indicies + """ + + @abstractmethod + def sort( + self, by: list[tuple[str, Literal["ascending", "descending"]]] | str + ) -> None: + """ + by: str sorts table by the data in the column with that name (asc). + by: list[tuple] sorts the table by the named column(s) with the directions + indicated. + """ + + +class ArrowBackend(DataTableBackend[pa.Table]): + def __init__(self, data: pa.Table, max_rows: int | None = None) -> None: + self._source_data = data + + # Arrow allows duplicate field names, but a table's to_pylist() and + # to_pydict() methods will drop duplicate-named fields! + field_names: list[str] = [] + renamed = False + for field in data.column_names: + n = 0 + while field in field_names: + field = f"{field}{n}" + renamed = True + n += 1 + field_names.append(field) + if renamed: + data = data.rename_columns(field_names) + + self._source_row_count = data.num_rows + if max_rows is not None and max_rows < self._source_row_count: + self.data = data.slice(offset=0, length=max_rows) + else: + self.data = data + self._console = Console() + self._column_content_widths: list[int] = [] + + @staticmethod + def _pydict_from_records( + records: Sequence[Iterable[Any]], has_header: bool = False + ) -> dict[str, list[Any]]: + headers = ( + records[0] + if has_header + else [f"f{i}" for i in range(len(list(records[0])))] + ) + data = list(map(list, records[1:] if has_header else records)) + pydict = {header: [row[i] for row in data] for i, header in enumerate(headers)} + return pydict + + @staticmethod + def _handle_overflow(scalar: pa.Scalar) -> Any | None: + """ + PyArrow may throw an OverflowError when casting arrow types + to python types; in some cases we can catch these and + present a sensible value in the data table; otherwise + we return None. + """ + if pt.is_date32(scalar.type): + if scalar.value > 0: # type: ignore[attr-defined] + return date.max + elif scalar.value <= 0: # type: ignore[attr-defined] + return date.min + elif pt.is_date64(scalar.type): + if scalar.value > 0: # type: ignore[attr-defined] + return date.max + elif scalar.value <= 0: # type: ignore[attr-defined] + return date.min + elif pt.is_timestamp(scalar.type): + if scalar.value > 0: # type: ignore[attr-defined] + return datetime.max + elif scalar.value <= 0: # type: ignore[attr-defined] + return datetime.min + + return None + + @classmethod + def from_batches( + cls, data: pa.RecordBatch, max_rows: int | None = None + ) -> "ArrowBackend": + tbl = pa.Table.from_batches([data]) + return cls(tbl, max_rows=max_rows) + + @classmethod + def from_parquet( + cls, path: Path | str, max_rows: int | None = None + ) -> "ArrowBackend": + tbl = pq.read_table(str(path)) + return cls(tbl, max_rows=max_rows) + + @classmethod + def from_pydict( + cls, data: Mapping[str, Sequence[Any]], max_rows: int | None = None + ) -> "ArrowBackend": + try: + tbl = pa.Table.from_pydict(dict(data)) + except (pal.ArrowInvalid, pal.ArrowTypeError): + # one or more fields has mixed types, like int and + # string. Cast all to string for safety + new_data = { + k: [str(val) if val is not None else None for val in v] + for k, v in data.items() + } + tbl = pa.Table.from_pydict(new_data) + return cls(tbl, max_rows=max_rows) + + @classmethod + def from_records( + cls, + records: Sequence[Iterable[Any]], + has_header: bool = False, + max_rows: int | None = None, + ) -> "ArrowBackend": + pydict = cls._pydict_from_records(records, has_header) + return cls.from_pydict(pydict, max_rows=max_rows) + + @property + def source_data(self) -> pa.Table: + return self._source_data + + @property + def source_row_count(self) -> int: + return self._source_row_count + + @property + def row_count(self) -> int: + return self.data.num_rows + + @property + def column_count(self) -> int: + return self.data.num_columns + + @property + def columns(self) -> Sequence[str]: + return self.data.column_names + + @property + def column_content_widths(self) -> list[int]: + if not self._column_content_widths: + measurements = [self._measure(arr) for arr in self.data.columns] + # pc.max returns None for each column without rows; we need to return 0 + # instead. + self._column_content_widths = [cw or 0 for cw in measurements] + + return self._column_content_widths + + def get_row_at(self, index: int) -> Sequence[Any]: + try: + row: Dict[str, Any] = self.data.slice(index, length=1).to_pylist()[0] + except OverflowError: + return [ + self._handle_overflow(self.data[i][index]) + for i in range(len(self.columns)) + ] + else: + return list(row.values()) + + def get_column_at(self, column_index: int) -> list[Any]: + try: + values = self.data[column_index].to_pylist() + except OverflowError: + # TODO: consider registering a scalar UDF here for parallel processing + return [self._handle_overflow(scalar) for scalar in self.data[column_index]] + else: + return values + + def get_cell_at(self, row_index: int, column_index: int) -> Any: + scalar = self.data[column_index][row_index] + try: + value = scalar.as_py() + except OverflowError: + value = self._handle_overflow(scalar) + return value + + def append_column(self, label: str, default: Any | None = None) -> int: + """ + Returns column index + """ + if default is None: + arr: pa.Array = pa.nulls(self.row_count) + else: + arr = pa.nulls(self.row_count, type=pa.string()) + arr = arr.fill_null(str(default)) + + self.data = self.data.append_column(label, arr) + if self._column_content_widths: + self._column_content_widths.append(measure_width(default, self._console)) + return self.data.num_columns - 1 + + def append_rows(self, records: Iterable[Iterable[Any]]) -> list[int]: + rows = list(records) + indicies = list(range(self.row_count, self.row_count + len(rows))) + records_with_headers = [self.data.column_names, *rows] + pydict = self._pydict_from_records(records_with_headers, has_header=True) + old_rows = self.data.to_batches() + new_rows = pa.RecordBatch.from_pydict( + pydict, + schema=self.data.schema, + ) + self.data = pa.Table.from_batches([*old_rows, new_rows]) + self._reset_content_widths() + return indicies + + def drop_row(self, row_index: int) -> None: + if row_index < 0 or row_index >= self.row_count: + raise IndexError(f"Can't drop row {row_index} of {self.row_count}") + above = self.data.slice(0, row_index).to_batches() + below = self.data.slice(row_index + 1).to_batches() + self.data = pa.Table.from_batches([*above, *below]) + self._reset_content_widths() + pass + + def update_cell(self, row_index: int, column_index: int, value: Any) -> None: + column = self.data.column(column_index) + pycolumn = self.get_column_at(column_index=column_index) + pycolumn[row_index] = value + new_type = pa.string() if pt.is_null(column.type) else column.type + self.data = self.data.set_column( + column_index, + self.data.column_names[column_index], + pa.array(pycolumn, type=new_type), + ) + if self._column_content_widths: + self._column_content_widths[column_index] = max( + measure_width(value, self._console), + self._column_content_widths[column_index], + ) + + def sort( + self, by: list[tuple[str, Literal["ascending", "descending"]]] | str + ) -> None: + """ + by: str sorts table by the data in the column with that name (asc). + by: list[tuple] sorts the table by the named column(s) with the directions + indicated. + """ + self.data = self.data.sort_by(by) + + def _reset_content_widths(self) -> None: + self._column_content_widths = [] + + def _measure(self, arr: pa._PandasConvertible) -> int: + # with some types we can measure the width more efficiently + if pt.is_boolean(arr.type): + return 7 + elif pt.is_null(arr.type): + return 0 + elif ( + pt.is_integer(arr.type) + or pt.is_floating(arr.type) + or pt.is_decimal(arr.type) + ): + try: + col_max = pc.max(arr.fill_null(0)).as_py() + except OverflowError: + col_max = 9223372036854775807 + try: + col_min = pc.min(arr.fill_null(0)).as_py() + except OverflowError: + col_min = -9223372036854775807 + return max([measure_width(el, self._console) for el in [col_max, col_min]]) + elif pt.is_temporal(arr.type): + try: + value = arr.drop_null()[0].as_py() + except OverflowError: + return 26 # need space for the infinity sign and a space + except IndexError: + return 24 + else: + # valid temporal types all have the same width for their type + return measure_width(value, self._console) + + # for everything else, we need to compute it + # First, cast the data to strings + try: + arr = arr.cast( + pa.string(), + safe=False, + ) + except (pal.ArrowNotImplementedError, pal.ArrowInvalid): + # some types can't be casted to strings natively by arrow, but they + # can be casted to strings by python. The arrow way is faster, but + # if it fails, register a python udf and try again + def py_str(_ctx: Any, arr: pa.Array) -> str | pa.Array | pa.ChunkedArray: + return pa.array([str(el) for el in arr], type=pa.string()) + + udf_name = f"tfdt_pystr_{arr.type}" + with suppress(pal.ArrowKeyError): # already registered + pc.register_scalar_function( + py_str, + function_name=udf_name, + function_doc={"summary": "str", "description": "built-in str"}, + in_types={"arr": arr.type}, + out_type=pa.string(), + ) + + arr = pc.call_function(udf_name, [arr]) + + # next, try to measure the UTF-encoded string length of each cell, + # then take the max + try: + width: int = pc.max(pc.utf8_length(arr.fill_null("")).fill_null(0)).as_py() + except OverflowError: + width = 10 + return width + + +if _HAS_POLARS: + + class PolarsBackend(DataTableBackend[pl.DataFrame]): + @classmethod + def from_file_path( + cls, path: Path, max_rows: int | None = None, has_header: bool = True + ) -> "PolarsBackend": + if path.suffix in [".arrow", ".feather"]: + tbl = pl.read_ipc(path) + elif path.suffix == ".arrows": + tbl = pl.read_ipc_stream(path) + elif path.suffix == ".json": + tbl = pl.read_json(path) + elif path.suffix == ".csv": + tbl = pl.read_csv(path, has_header=has_header) + else: + raise TypeError( + f"Dont know how to load file type {path.suffix} for {path}" + ) + return cls(tbl, max_rows=max_rows) + + @classmethod + def from_pydict( + cls, pydict: Mapping[str, Sequence[Any]], max_rows: int | None = None + ) -> "PolarsBackend": + return cls(pl.from_dict(pydict), max_rows=max_rows) + + @classmethod + def from_dataframe( + cls, frame: pl.DataFrame, max_rows: int | None = None + ) -> "PolarsBackend": + return cls(frame, max_rows=max_rows) + + def __init__(self, data: pl.DataFrame, max_rows: int | None = None) -> None: + self._source_data = data + + # Arrow allows duplicate field names, but a table's to_pylist() and + # to_pydict() methods will drop duplicate-named fields! + field_names: list[str] = [] + for field in data.columns: + n = 0 + while field in field_names: + field = f"{field}{n}" + n += 1 + field_names.append(field) + data.columns = field_names + + self._source_row_count = len(data) + if max_rows is not None and max_rows < self._source_row_count: + self.data = data.slice(offset=0, length=max_rows) + else: + self.data = data + self._console = Console() + self._column_content_widths: list[int] = [] + + @property + def source_data(self) -> pl.DataFrame: + return self._source_data + + @property + def source_row_count(self) -> int: + return self._source_row_count + + @property + def row_count(self) -> int: + return len(self.data) + + @property + def column_count(self) -> int: + return len(self.data.columns) + + @property + def columns(self) -> Sequence[str]: + return self.data.columns + + def get_row_at(self, index: int) -> Sequence[Any]: + if index < 0 or index >= len(self.data): + raise IndexError( + f"Cannot get row={index} in table with {len(self.data)} rows " + f"and {len(self.data.columns)} cols" + ) + return list(self.data.slice(index, length=1).to_dicts()[0].values()) + + def get_column_at(self, column_index: int) -> Sequence[Any]: + if column_index < 0 or column_index >= len(self.data.columns): + raise IndexError( + f"Cannot get column={column_index} in table with {len(self.data)} " + f"rows and {len(self.data.columns)} cols." + ) + return list(self.data.to_series(column_index)) + + def get_cell_at(self, row_index: int, column_index: int) -> Any: + if ( + row_index >= len(self.data) + or row_index < 0 + or column_index < 0 + or column_index >= len(self.data.columns) + ): + raise IndexError( + f"Cannot get cell at row={row_index} col={column_index} in table " + f"with {len(self.data)} rows and {len(self.data.columns)} cols" + ) + return self.data.to_series(column_index)[row_index] + + def drop_row(self, row_index: int) -> None: + if row_index < 0 or row_index >= self.row_count: + raise IndexError(f"Can't drop row {row_index} of {self.row_count}") + above = self.data.slice(0, row_index) + below = self.data.slice(row_index + 1) + self.data = pl.concat([above, below]) + self._reset_content_widths() + + def append_rows(self, records: Iterable[Iterable[Any]]) -> list[int]: + rows_to_add = pl.from_dicts( + [dict(zip(self.data.columns, row)) for row in records] + ) + indicies = list(range(self.row_count, self.row_count + len(rows_to_add))) + self.data = pl.concat([self.data, rows_to_add]) + self._reset_content_widths() + return indicies + + def append_column(self, label: str, default: Any | None = None) -> int: + """ + Returns column index + """ + self.data = self.data.with_columns( + pl.Series([default]) + .extend_constant(default, self.row_count - 1) + .alias(label) + ) + if self._column_content_widths: + self._column_content_widths.append( + measure_width(default, self._console) + ) + return len(self.data.columns) - 1 + + def _reset_content_widths(self) -> None: + self._column_content_widths = [] + + def update_cell(self, row_index: int, column_index: int, value: Any) -> None: + if row_index >= len(self.data) or column_index >= len(self.data.columns): + raise IndexError( + f"Cannot update cell at row={row_index} col={column_index} in " + f"table with {len(self.data)} rows and " + f"{len(self.data.columns)} cols" + ) + col_name = self.data.columns[column_index] + self.data = self.data.with_columns( + self.data.to_series(column_index) + .scatter(row_index, value) + .alias(col_name) + ) + if self._column_content_widths: + self._column_content_widths[column_index] = max( + measure_width(value, self._console), + self._column_content_widths[column_index], + ) + + @property + def column_content_widths(self) -> list[int]: + if not self._column_content_widths: + measurements = [ + self._measure(self.data[arr]) for arr in self.data.columns + ] + # pc.max returns None for each column without rows; we need to return 0 + # instead. + self._column_content_widths = [cw or 0 for cw in measurements] + + return self._column_content_widths + + def _measure(self, arr: pl.Series) -> int: + # with some types we can measure the width more efficiently + dtype = arr.dtype + if dtype == pld.Categorical(): + return self._measure(arr.cat.get_categories()) + + if dtype.is_decimal() or dtype.is_float() or dtype.is_integer(): + col_max = arr.max() + col_min = arr.min() + return max( + [measure_width(el, self._console) for el in [col_max, col_min]] + ) + if dtype.is_temporal(): + try: + value = arr.drop_nulls()[0] + except IndexError: + return 0 + else: + return measure_width(value, self._console) + if dtype.is_(pld.Boolean()): + return 7 + + # for everything else, we need to compute it + + arr = arr.cast( + pl.Utf8(), + strict=False, + ) + width = arr.fill_null("").str.len_chars().max() + assert isinstance(width, int) + return width + + def sort( + self, by: list[tuple[str, Literal["ascending", "descending"]]] | str + ) -> None: + """ + by: str sorts table by the data in the column with that name (asc). + by: list[tuple] sorts the table by the named column(s) with the directions + indicated. + """ + if isinstance(by, str): + cols = [by] + typs = [False] + else: + cols = [x for x, _ in by] + typs = [x == "descending" for _, x in by] + self.data = self.data.sort(cols, descending=typs) diff --git a/src/textual_fastdatatable/column.py b/src/textual_fastdatatable/column.py new file mode 100644 index 0000000..24d9d97 --- /dev/null +++ b/src/textual_fastdatatable/column.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import re +from dataclasses import dataclass + +from rich.text import Text + +CELL_X_PADDING = 2 + +SNAKE_ID_PROG = re.compile(r"(\b|_)id\b", flags=re.IGNORECASE) +CAMEL_ID_PROG = re.compile(r"[a-z]I[dD]\b") + + +@dataclass +class Column: + """Metadata for a column in the DataTable.""" + + label: Text + width: int = 0 + content_width: int = 0 + auto_width: bool = False + max_content_width: int | None = None + + def __post_init__(self) -> None: + self._is_id: bool | None = None + + @property + def render_width(self) -> int: + """Width in cells, required to render a column.""" + # +2 is to account for space padding either side of the cell + if self.auto_width and self.max_content_width is not None: + return ( + min(max(len(self.label), self.content_width), self.max_content_width) + + CELL_X_PADDING + ) + elif self.auto_width: + return max(len(self.label), self.content_width) + CELL_X_PADDING + else: + return self.width + CELL_X_PADDING + + @property + def is_id(self) -> bool: + if self._is_id is None: + snake_id = SNAKE_ID_PROG.search(str(self.label)) is not None + camel_id = CAMEL_ID_PROG.search(str(self.label)) is not None + self._is_id = snake_id or camel_id + return self._is_id diff --git a/src/textual_fastdatatable/data_table.py b/src/textual_fastdatatable/data_table.py new file mode 100644 index 0000000..e582c32 --- /dev/null +++ b/src/textual_fastdatatable/data_table.py @@ -0,0 +1,2808 @@ +# Forked from Textual; the original code comes with the following License: + +# MIT License + +# Copyright (c) 2021 Will McGugan + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from __future__ import annotations + +import functools +from datetime import date, datetime, time, timedelta +from decimal import Decimal +from itertools import chain, zip_longest +from typing import Any, ClassVar, Iterable, NamedTuple, Tuple, Union + +import rich.repr +from rich.console import RenderableType +from rich.padding import Padding +from rich.pretty import Pretty +from rich.segment import Segment +from rich.style import Style +from rich.text import Text, TextType +from textual import events, on +from textual._segment_tools import line_crop +from textual._two_way_dict import TwoWayDict +from textual._types import SegmentLines +from textual.binding import Binding, BindingType +from textual.cache import LRUCache +from textual.color import Color +from textual.coordinate import Coordinate +from textual.geometry import Region, Size, Spacing, clamp +from textual.message import Message +from textual.reactive import Reactive +from textual.render import measure +from textual.renderables.styled import Styled +from textual.scroll_view import ScrollView +from textual.strip import Strip +from textual.widget import PseudoClasses +from typing_extensions import Literal, Self + +from textual_fastdatatable.backend import DataTableBackend, create_backend +from textual_fastdatatable.column import Column +from textual_fastdatatable.formatter import cell_formatter, measure_width + +CursorType = Literal["cell", "range", "row", "column", "none"] +"""The valid types of cursors for +[`DataTable.cursor_type`][textual.widgets.DataTable.cursor_type].""" +TooltipCacheKey = Tuple[int, int, int] +CellCacheKey = Tuple[int, int, Style, bool, bool, bool, bool, int, PseudoClasses] +LineCacheKey = Tuple[ + int, + int, + int, + int, + Coordinate, + Coordinate, + Union[Coordinate, None], + Style, + CursorType, + bool, + int, + PseudoClasses, +] +RowCacheKey = Tuple[ + int, + int, + int, + int, + Style, + Coordinate, + Coordinate, + Union[Coordinate, None], + CursorType, + bool, + bool, + int, + PseudoClasses, +] +CellType = RenderableType + + +class CellDoesNotExist(Exception): + """The cell key/index was invalid. + + Raised when the coordinates or cell key provided does not exist + in the DataTable (e.g. out of bounds index, invalid key)""" + + +class RowDoesNotExist(Exception): + """Raised when the row index or row key provided does not exist + in the DataTable (e.g. out of bounds index, invalid key)""" + + +class ColumnDoesNotExist(Exception): + """Raised when the column index or column key provided does not exist + in the DataTable (e.g. out of bounds index, invalid key)""" + + +class RowRenderables(NamedTuple): + """Container for a row, which contains an optional label and some data cells.""" + + label: RenderableType | None + cells: list[RenderableType] + + +class DataTable(ScrollView, can_focus=True): + """A tabular widget that contains data.""" + + BINDINGS: ClassVar[list[BindingType]] = [ + Binding("enter", "select_cursor", "Select", show=False), + Binding("up", "cursor_up", "Cursor Up", show=False), + Binding("down", "cursor_down", "Cursor Down", show=False), + Binding("ctrl+up", "scroll_home", "Home", show=False), + Binding("ctrl+down", "scroll_end", "End", show=False), + Binding("right", "cursor_right", "Cursor Right", show=False), + Binding("left", "cursor_left", "Cursor Left", show=False), + Binding("tab", "cursor_next", "Cursor Next", show=False), + Binding("shift+tab", "cursor_prev", "Cursor Prev", show=False), + Binding("ctrl+right", "cursor_row_end", "Cursor Right", show=False), + Binding("ctrl+left", "cursor_row_start", "Cursor Left", show=False), + Binding("pageup", "page_up", "Page Up", show=False), + Binding("pagedown", "page_down", "Page Down", show=False), + Binding("home", "scroll_home", "Home", show=False), + Binding("end", "scroll_end", "End", show=False), + Binding("ctrl+home", "cursor_table_start", "Home", show=False), + Binding("ctrl+end", "cursor_table_end", "End", show=False), + Binding("shift+up", "cursor_up(True)", "Cursor Up", show=False), + Binding("shift+down", "cursor_down(True)", "Cursor Down", show=False), + Binding("shift+right", "cursor_right(True)", "Cursor Right", show=False), + Binding("shift+left", "cursor_left(True)", "Cursor Left", show=False), + Binding("shift+pageup", "page_up(True)", "Page Up", show=False), + Binding("shift+pagedown", "page_down(True)", "Page Down", show=False), + Binding("shift+home", "scroll_home(True)", "Home", show=False), + Binding("shift+end", "scroll_end(True)", "End", show=False), + Binding("ctrl+shift+up", "scroll_home(True)", "Home", show=False), + Binding("ctrl+shift+down", "scroll_end(True)", "End", show=False), + Binding("ctrl+shift+right", "cursor_row_end(True)", "Cursor Right", show=False), + Binding("ctrl+shift+left", "cursor_row_start(True)", "Cursor Left", show=False), + Binding("ctrl+shift+home", "cursor_table_start(True)", "Home", show=False), + Binding("ctrl+shift+end", "cursor_table_end(True)", "End", show=False), + Binding("ctrl+a", "select_all", "Select All", show=False), + Binding("ctrl+c", "copy_selection", "Copy", show=False), + ] + """ + | Key(s) | Description | + | :- | :- | + | enter | Select cells under the cursor. | + | up | Move the cursor up. | + | down | Move the cursor down. | + | right | Move the cursor right. | + | left | Move the cursor left. | + """ + + COMPONENT_CLASSES: ClassVar[set[str]] = { + "datatable--cursor", + "datatable--selectrange", + "datatable--hover", + "datatable--fixed", + "datatable--fixed-cursor", + "datatable--header", + "datatable--header-cursor", + "datatable--header-selectrange", + "datatable--header-hover", + "datatable--odd-row", + "datatable--even-row", + } + """ + | Class | Description | + | :- | :- | + | `datatable--cursor` | Target the cursor. | + | `datatable--selectrange` | Target the cells in the selection range (ex cursor). | + | `datatable--hover` | Target the cells under the hover cursor. | + | `datatable--fixed` | Target fixed columns and fixed rows. | + | `datatable--fixed-cursor` | Target highlighted and fixed columns or header. | + | `datatable--header` | Target the header of the data table. | + | `datatable--header-cursor` | Target header cells highlighted by the cursor. | + | `datatable--header-selectrange` | Target header cells highlighted by the range. | + | `datatable--header-hover` | Target hovered header or row label cells. | + | `datatable--even-row` | Target even rows (row indices start at 0). | + | `datatable--odd-row` | Target odd rows (row indices start at 0). | + """ + + DEFAULT_CSS = """ + DataTable:dark { + background: initial; + } + DataTable { + background: $surface ; + color: $text; + height: auto; + max-height: 100vh; + } + DataTable > .datatable--header { + text-style: bold; + background: $primary; + color: $text; + } + DataTable > .datatable--fixed { + background: $primary 50%; + color: $text; + } + + DataTable > .datatable--odd-row { + + } + + DataTable > .datatable--even-row { + background: $primary 10%; + } + + DataTable > .datatable--cursor { + background: $secondary; + color: $text; + } + + DataTable > .datatable--selectrange { + background: $secondary 50%; + color: $text; + } + + DataTable > .datatable--fixed-cursor { + background: $secondary 92%; + color: $text; + } + + DataTable > .datatable--header-cursor { + background: $secondary-darken-1; + color: $text; + } + + DataTable > .datatable--header-cursor { + background: $secondary 50%; + color: $text; + } + + DataTable > .datatable--header-hover { + background: $secondary 30%; + } + + DataTable:dark > .datatable--even-row { + background: $primary 15%; + } + + DataTable > .datatable--hover { + background: $secondary 20%; + } + """ + + show_header = Reactive(True) + show_row_labels = Reactive(True) + fixed_rows = Reactive(0) + fixed_columns = Reactive(0) + zebra_stripes = Reactive(False) + header_height = Reactive(1) + show_cursor = Reactive(True) + cursor_type: Reactive[CursorType] = Reactive[CursorType]("cell") + """The type of the cursor of the `DataTable`.""" + + cursor_coordinate: Reactive[Coordinate] = Reactive( + Coordinate(0, 0), repaint=False, always_update=True + ) + """Current cursor [`Coordinate`][textual.coordinate.Coordinate]. + + This can be set programmatically or changed via the method + [`move_cursor`][textual.widgets.DataTable.move_cursor]. + """ + selection_anchor_coordinate: Reactive[Coordinate | None] = Reactive( + None, repaint=False, always_update=True + ) + hover_coordinate: Reactive[Coordinate] = Reactive( + Coordinate(0, 0), repaint=False, always_update=True + ) + """The coordinate of the `DataTable` that is being hovered.""" + + class DataLoadError(Message): + def __init__(self, error: Exception) -> None: + super().__init__() + self.error = error + + class CellHighlighted(Message): + """Posted when the cursor moves to highlight a new cell. + + This is only relevant when the `cursor_type` is `"cell"`. + It's also posted when the cell cursor is + re-enabled (by setting `show_cursor=True`), and when the cursor type is + changed to `"cell"`. Can be handled using `on_data_table_cell_highlighted` in + a subclass of `DataTable` or in a parent widget in the DOM. + """ + + def __init__( + self, + data_table: DataTable, + value: CellType, + coordinate: Coordinate, + ) -> None: + self.data_table = data_table + """The data table.""" + self.value: CellType = value + """The value in the highlighted cell.""" + self.coordinate: Coordinate = coordinate + """The coordinate of the highlighted cell.""" + super().__init__() + + def __rich_repr__(self) -> rich.repr.Result: + yield "value", self.value + yield "coordinate", self.coordinate + + @property + def control(self) -> DataTable: + """Alias for the data table.""" + return self.data_table + + class CellSelected(Message): + """Posted by the `DataTable` widget when a cell is selected. + + This is only relevant when the `cursor_type` is `"cell"`. Can be handled using + `on_data_table_cell_selected` in a subclass of `DataTable` or in a parent + widget in the DOM. + """ + + def __init__( + self, + data_table: DataTable, + value: CellType, + coordinate: Coordinate, + ) -> None: + self.data_table = data_table + """The data table.""" + self.value: CellType = value + """The value in the cell that was selected.""" + self.coordinate: Coordinate = coordinate + """The coordinate of the cell that was selected.""" + super().__init__() + + def __rich_repr__(self) -> rich.repr.Result: + yield "value", self.value + yield "coordinate", self.coordinate + + @property + def control(self) -> DataTable: + """Alias for the data table.""" + return self.data_table + + class RowHighlighted(Message): + """Posted when a row is highlighted. + + This message is only posted when the + `cursor_type` is set to `"row"`. Can be handled using + `on_data_table_row_highlighted` in a subclass of `DataTable` or in a parent + widget in the DOM. + """ + + def __init__(self, data_table: DataTable, cursor_row: int) -> None: + self.data_table = data_table + """The data table.""" + self.cursor_row: int = cursor_row + """The y-coordinate of the cursor that highlighted the row.""" + super().__init__() + + def __rich_repr__(self) -> rich.repr.Result: + yield "cursor_row", self.cursor_row + + @property + def control(self) -> DataTable: + """Alias for the data table.""" + return self.data_table + + class RowSelected(Message): + """Posted when a row is selected. + + This message is only posted when the + `cursor_type` is set to `"row"`. Can be handled using + `on_data_table_row_selected` in a subclass of `DataTable` or in a parent + widget in the DOM. + """ + + def __init__(self, data_table: DataTable, cursor_row: int) -> None: + self.data_table = data_table + """The data table.""" + self.cursor_row: int = cursor_row + """The y-coordinate of the cursor that made the selection.""" + super().__init__() + + def __rich_repr__(self) -> rich.repr.Result: + yield "cursor_row", self.cursor_row + + @property + def control(self) -> DataTable: + """Alias for the data table.""" + return self.data_table + + class ColumnHighlighted(Message): + """Posted when a column is highlighted. + + This message is only posted when the + `cursor_type` is set to `"column"`. Can be handled using + `on_data_table_column_highlighted` in a subclass of `DataTable` or in a parent + widget in the DOM. + """ + + def __init__(self, data_table: DataTable, cursor_column: int) -> None: + self.data_table = data_table + """The data table.""" + self.cursor_column: int = cursor_column + """The x-coordinate of the column that was highlighted.""" + super().__init__() + + def __rich_repr__(self) -> rich.repr.Result: + yield "cursor_column", self.cursor_column + + @property + def control(self) -> DataTable: + """Alias for the data table.""" + return self.data_table + + class ColumnSelected(Message): + """Posted when a column is selected. + + This message is only posted when the + `cursor_type` is set to `"column"`. Can be handled using + `on_data_table_column_selected` in a subclass of `DataTable` or in a parent + widget in the DOM. + """ + + def __init__(self, data_table: DataTable, cursor_column: int) -> None: + self.data_table = data_table + """The data table.""" + self.cursor_column: int = cursor_column + """The x-coordinate of the column that was selected.""" + super().__init__() + + def __rich_repr__(self) -> rich.repr.Result: + yield "cursor_column", self.cursor_column + + @property + def control(self) -> DataTable: + """Alias for the data table.""" + return self.data_table + + class HeaderSelected(Message): + """Posted when a column header/label is clicked.""" + + def __init__( + self, + data_table: DataTable, + column_index: int, + label: Text, + ): + self.data_table = data_table + """The data table.""" + self.column_index = column_index + """The index for the column.""" + self.label = label + """The text of the label.""" + super().__init__() + + def __rich_repr__(self) -> rich.repr.Result: + yield "column_index", self.column_index + yield "label", self.label.plain + + @property + def control(self) -> DataTable: + """Alias for the data table.""" + return self.data_table + + class RowLabelSelected(Message): + """Posted when a row label is clicked.""" + + def __init__( + self, + data_table: DataTable, + row_index: int, + label: Text, + ): + self.data_table = data_table + """The data table.""" + self.row_index = row_index + """The index for the column.""" + self.label = label + """The text of the label.""" + super().__init__() + + def __rich_repr__(self) -> rich.repr.Result: + yield "row_index", self.row_index + yield "label", self.label.plain + + @property + def control(self) -> DataTable: + """Alias for the data table.""" + return self.data_table + + class SelectionCopied(Message): + """Posted when the user presses ctrl+c.""" + + def __init__( + self, + data_table: DataTable, + values: list[tuple[Any, ...]], + ): + self.data_table = data_table + """The data table.""" + self.values = values + """The values of the selected cells.""" + super().__init__() + + def __rich_repr__(self) -> rich.repr.Result: + yield "values", self.values + + @property + def control(self) -> DataTable: + """Alias for the data table.""" + return self.data_table + + def __init__( + self, + *, + backend: DataTableBackend | None = None, + data: Any | None = None, + column_labels: list[str | Text] | None = None, + column_widths: list[int | None] | None = None, + max_column_content_width: int | None = None, + show_header: bool = True, + show_row_labels: bool = True, + max_rows: int | None = None, + fixed_rows: int = 0, + fixed_columns: int = 0, + zebra_stripes: bool = False, + header_height: int = 1, + show_cursor: bool = True, + cursor_foreground_priority: Literal["renderable", "css"] = "css", + cursor_background_priority: Literal["renderable", "css"] = "renderable", + cursor_type: CursorType = "cell", + name: str | None = None, + id: str | None = None, # noqa: A002 + classes: str | None = None, + disabled: bool = False, + null_rep: str = "", + render_markup: bool = True, + ) -> None: + super().__init__(name=name, id=id, classes=classes, disabled=disabled) + try: + self.backend: DataTableBackend | None = ( + backend + if backend is not None + else create_backend( + data, + max_rows=max_rows, + has_header=(column_labels is None), + ) + ) + except (TypeError, OSError) as e: + self.backend = None + if data is not None: + self.post_message(self.DataLoadError(e)) + + self._column_labels: list[str | Text] | None = ( + list(column_labels) if column_labels is not None else None + ) + self._column_widths: list[int | None] | None = ( + list(column_widths) if column_widths is not None else None + ) + self.max_column_content_width: int | None = max_column_content_width + self._ordered_columns: None | list[Column] = None + + self._row_render_cache: LRUCache[ + RowCacheKey, tuple[SegmentLines, SegmentLines] + ] = LRUCache(1000) + """For each row (a row can have a height of multiple lines), we maintain a + cache of the fixed and scrollable lines within that row to minimise how often + we need to re-render it. """ + self._cell_render_cache: LRUCache[CellCacheKey, SegmentLines] = LRUCache(10000) + """Cache for individual cells.""" + self._line_cache: LRUCache[LineCacheKey, Strip] = LRUCache(1000) + """Cache for lines within rows.""" + self._tooltip_cache: LRUCache[TooltipCacheKey, RenderableType | None] = ( + LRUCache(1000) + ) + """Cache for values for the tooltip""" + # self._offset_cache: LRUCache[int, list[tuple[RowKey, int]]] = LRUCache(1) + """Cached y_offset - key is update_count - see y_offsets property for more + information """ + # self._ordered_row_cache: LRUCache[tuple[int, int], list[Row]] = LRUCache(1) + """Caches row ordering - key is (num_rows, update_count).""" + + self._pseudo_class_state = PseudoClasses(False, False, False) + """The pseudo-class state is used as part of cache keys to ensure that, + for example, when we lose focus on the DataTable, rules which apply to :focus + are invalidated and we prevent lingering styles.""" + + self._require_update_dimensions: bool = True + """Set to re-calculate dimensions on idle.""" + # TODO: support mutable data + # self._new_rows: set[RowKey] = set() + """Tracking newly added rows to be used in calculation of dimensions on idle.""" + # self._updated_cells: set[CellKey] = set() + """Track which cells were updated, so that we can refresh them once on idle.""" + + self._show_hover_cursor = False + """Used to hide the mouse hover cursor when the user uses the keyboard.""" + self._update_count = 0 + """Number of update (INCLUDING SORT) operations so far. + Used for cache invalidation.""" + self._header_row_index = -1 + """The header is a special row - not part of the data. Retrieve via this key.""" + self._label_column_index = -1 + """The column containing row labels is not part of the data. + This key identifies it.""" + self._labelled_row_exists = False + """Whether or not the user has supplied any rows with labels.""" + self._label_column = Column(Text(), auto_width=True) + """The largest content width out of all row labels in the table.""" + + self.show_header = show_header + """Show/hide the header row (the row of column labels).""" + self.show_row_labels = show_row_labels + """Show/hide the column containing the labels of rows.""" + self.header_height = header_height + """The height of the header row (the row of column labels).""" + self.fixed_rows = fixed_rows + """The number of rows to fix (prevented from scrolling).""" + self.fixed_columns = fixed_columns + """The number of columns to fix (prevented from scrolling).""" + self.zebra_stripes = zebra_stripes + """Apply zebra effect on row backgrounds (light, dark, light, dark, ...).""" + self.show_cursor = show_cursor + """Show/hide both the keyboard and hover cursor.""" + self.cursor_foreground_priority = cursor_foreground_priority + """Should we prioritize the cursor component class CSS foreground or the + renderable foreground in the event where a cell contains a renderable with a + foreground color.""" + self.cursor_background_priority = cursor_background_priority + """Should we prioritize the cursor component class CSS background or the + renderable background in the event where a cell contains a renderable with a + background color.""" + self.cursor_type = cursor_type + """The type of cursor of the `DataTable`.""" + self.null_rep = Text.from_markup(null_rep) + """The string used to represent missing data (None or null)""" + self.render_markup = render_markup + """If true, render string data as Rich markup.""" + + @property + def hover_row(self) -> int: + """The index of the row that the mouse cursor is currently hovering above.""" + return self.hover_coordinate.row + + @property + def hover_column(self) -> int: + """The index of the column that the mouse cursor is currently hovering + above.""" + return self.hover_coordinate.column + + @property + def cursor_row(self) -> int: + """The index of the row that the DataTable cursor is currently on.""" + return self.cursor_coordinate.row + + @property + def cursor_column(self) -> int: + """The index of the column that the DataTable cursor is currently on.""" + return self.cursor_coordinate.column + + @property + def selection_anchor_row(self) -> int | None: + """The index of the row that the DataTable select range starts on.""" + if self.selection_anchor_coordinate is None: + return None + return self.selection_anchor_coordinate.row + + @property + def selection_anchor_column(self) -> int | None: + """The index of the column that the DataTable select range starts on.""" + if self.selection_anchor_coordinate is None: + return None + return self.selection_anchor_coordinate.column + + @property + def source_row_count(self) -> int: + """The number of rows in the data source used to create the table.""" + if self.backend is None: + return 0 + else: + return self.backend.source_row_count + + @property + def row_count(self) -> int: + """The number of rows currently present in the DataTable.""" + if self.backend is None: + return 0 + else: + return self.backend.row_count + + @property + def _total_row_height(self) -> int: + """ + The total height of all rows within the DataTable, NOT including the header. + """ + # TODO: support rows with height > 1 + return self.row_count + + @property + def column_count(self) -> int: + if self.backend is not None: + return self.backend.column_count + elif self._column_labels is not None: + return len(self._column_labels) + else: + return 0 + + def update_cell( + self, + row_index: int, + column_index: int, + value: CellType, + *, + update_width: bool = False, + ) -> None: + """Update the cell identified by the specified row key and column key. + + Args: + row_key: The key identifying the row. + column_key: The key identifying the column. + value: The new value to put inside the cell. + update_width: Whether to resize the column width to accommodate + for the new cell content. + + Raises: + CellDoesNotExist: When the supplied `row_key` and `column_key` + cannot be found in the table. + """ + if self.backend is None: + raise CellDoesNotExist("No data in the table") + try: + self.backend.update_cell(row_index, column_index, value) + except IndexError: + raise CellDoesNotExist( + f"No cell exists for row_key={row_index}, column_key={column_index}." + ) from None + self._update_count += 1 + + # Recalculate widths if necessary + if update_width: + self._require_update_dimensions = True + + self.refresh() + + def update_cell_at( + self, coordinate: Coordinate, value: CellType, *, update_width: bool = False + ) -> None: + """Update the content inside the cell currently occupying the given coordinate. + + Args: + coordinate: The coordinate to update the cell at. + value: The new value to place inside the cell. + update_width: Whether to resize the column width to accommodate + for the new cell content. + """ + raise NotImplementedError("No updates allowed.") + if not self.is_valid_coordinate(coordinate): + raise CellDoesNotExist(f"Coordinate {coordinate!r} is invalid.") + + row_key, column_key = self.coordinate_to_cell_key(coordinate) + self.update_cell(row_key, column_key, value, update_width=update_width) + + def get_cell_at(self, coordinate: Coordinate) -> Any: + """Get the value from the cell occupying the given coordinate. + + Args: + coordinate: The coordinate to retrieve the value from. + + Returns: + The value of the cell at the coordinate. + + Raises: + CellDoesNotExist: If there is no cell with the given coordinate. + """ + if self.backend is None: + raise CellDoesNotExist("No data in the table") + try: + return self.backend.get_cell_at(coordinate.row, coordinate.column) + except IndexError as e: + raise CellDoesNotExist(f"No cell exists at coordinate {coordinate}.") from e + + # def get_row(self, row_key: RowKey | str) -> list[CellType]: + # """Get the values from the row identified by the given row key. + + # Args: + # row_key: The key of the row. + + # Returns: + # A list of the values contained within the row. + + # Raises: + # RowDoesNotExist: When there is no row corresponding to the key. + # """ + # raise NotImplementedError("Use get_row_at instead.") + # if row_key not in self._row_locations: + # raise RowDoesNotExist(f"Row key {row_key!r} is not valid.") + # cell_mapping: dict[ColumnKey, CellType] = self._data.get(row_key, {}) + # ordered_row: list[CellType] = [ + # cell_mapping[column.key] for column in self.ordered_columns + # ] + # return ordered_row + + def get_row_at(self, row_index: int) -> list[Any]: + """Get the values from the cells in a row at a given index. This will + return the values from a row based on the rows _current position_ in + the table. + + Args: + row_index: The index of the row. + + Returns: + A list of the values contained in the row. + + Raises: + RowDoesNotExist: If there is no row with the given index. + """ + if self.backend is None or not self.is_valid_row_index(row_index): + raise RowDoesNotExist(f"Row index {row_index!r} is not valid.") + return list(self.backend.get_row_at(row_index)) + + # def get_column(self, column_key: ColumnKey | str) -> Iterable[CellType]: + # """Get the values from the column identified by the given column key. + + # Args: + # column_key: The key of the column. + + # Returns: + # A generator which yields the cells in the column. + + # Raises: + # ColumnDoesNotExist: If there is no column corresponding to the key. + # """ + # raise NotImplementedError("Use get_column_at instead.") + # if column_key not in self._column_locations: + # raise ColumnDoesNotExist(f"Column key {column_key!r} is not valid.") + + # data = self._data + # for row_metadata in self.ordered_rows: + # row_key = row_metadata.key + # yield data[row_key][column_key] + + def get_column_at(self, column_index: int) -> Iterable[Any]: + """Get the values from the column at a given index. + + Args: + column_index: The index of the column. + + Returns: + A generator which yields the cells in the column. + + Raises: + ColumnDoesNotExist: If there is no column with the given index. + """ + if self.backend is None or not self.is_valid_column_index(column_index): + raise ColumnDoesNotExist(f"Column index {column_index!r} is not valid.") + + yield from self.backend.get_column_at(column_index) + + def _clear_caches(self) -> None: + self._row_render_cache.clear() + self._cell_render_cache.clear() + self._line_cache.clear() + self._styles_cache.clear() + self._tooltip_cache.clear() + # self._offset_cache.clear() + # self._ordered_row_cache.clear() + self._get_styles_to_render_cell.cache_clear() + + def get_row_height(self, row_index: int) -> int: + """Given a row key, return the height of that row in terminal cells. + + Args: + row_key: The key of the row. + + Returns: + The height of the row, measured in terminal character cells. + """ + return 1 + # TODO: support variable height rows. + # if row_key is self._header_row_key: + # return self.header_height + # return self.rows[row_key].height + + def notify_style_update(self) -> None: + self._clear_caches() + self.refresh() + + def _on_resize(self, _: events.Resize) -> None: + self._update_count += 1 + + def watch_show_cursor(self, show_cursor: bool) -> None: + self._clear_caches() + if show_cursor and self.cursor_type != "none": + # When we re-enable the cursor, apply highlighting and + # post the appropriate [Row|Column|Cell]Highlighted event. + self._scroll_cursor_into_view(animate=False) + if self.cursor_type == "cell": + self._highlight_coordinate(self.cursor_coordinate) + elif self.cursor_type == "range": + self._highlight_range( + self.cursor_coordinate, self.selection_anchor_coordinate + ) + elif self.cursor_type == "row": + self._highlight_row(self.cursor_row) + elif self.cursor_type == "column": + self._highlight_column(self.cursor_column) + + def watch_show_header(self, show: bool) -> None: + width, height = self.virtual_size + height_change = self.header_height if show else -self.header_height + self.virtual_size = Size(width, height + height_change) + self._scroll_cursor_into_view() + self._clear_caches() + + def watch_show_row_labels(self, show: bool) -> None: + width, height = self.virtual_size + column_width = self._label_column.render_width + width_change = column_width if show else -column_width + self.virtual_size = Size(width + width_change, height) + self._scroll_cursor_into_view() + self._clear_caches() + + def watch_fixed_rows(self) -> None: + self._clear_caches() + + def watch_fixed_columns(self) -> None: + self._clear_caches() + + def watch_zebra_stripes(self) -> None: + self._clear_caches() + + def watch_hover_coordinate(self, old: Coordinate, value: Coordinate) -> None: + self.refresh_coordinate(old) + self.refresh_coordinate(value) + + def watch_cursor_coordinate( + self, old_coordinate: Coordinate, new_coordinate: Coordinate + ) -> None: + if old_coordinate != new_coordinate: + # Refresh the old and the new cell, and post the appropriate + # message to tell users of the newly highlighted row/cell/column. + if self.cursor_type == "cell": + self.refresh_coordinate(old_coordinate) + self._highlight_coordinate(new_coordinate) + elif self.cursor_type == "range": + self.refresh_range(old_coordinate, self.selection_anchor_coordinate) + self._highlight_range(new_coordinate, self.selection_anchor_coordinate) + elif self.cursor_type == "row": + self.refresh_row(old_coordinate.row) + self._highlight_row(new_coordinate.row) + elif self.cursor_type == "column": + self.refresh_column(old_coordinate.column) + self._highlight_column(new_coordinate.column) + # If the coordinate was changed via `move_cursor`, give priority to its + # scrolling because it may be animated. + self.call_next(self._scroll_cursor_into_view) + + def watch_selection_anchor_coordinate( + self, old_coordinate: Coordinate | None, new_coordinate: Coordinate | None + ) -> None: + if self.cursor_type == "range" and old_coordinate != new_coordinate: + # Refresh the old and the new cell, and post the appropriate + # message to tell users of the newly highlighted row/cell/column. + self.refresh_range(self.cursor_coordinate, old_coordinate) + self._highlight_range(self.cursor_coordinate, new_coordinate) + # If the coordinate was changed via `move_cursor`, give priority to its + # scrolling because it may be animated. + self.call_next(self._scroll_cursor_into_view) + + def move_cursor( + self, + *, + row: int | None = None, + column: int | None = None, + animate: bool = False, + select: bool = False, + ) -> None: + """Move the cursor to the given position. + + Example: + ```py + datatable = app.query_one(DataTable) + datatable.move_cursor(row=4, column=6) + # datatable.cursor_coordinate == Coordinate(4, 6) + datatable.move_cursor(row=3) + # datatable.cursor_coordinate == Coordinate(3, 6) + ``` + + Args: + row: The new row to move the cursor to. + column: The new column to move the cursor to. + animate: Whether to animate the change of coordinates. + """ + if select and self.selection_anchor_coordinate is None: + self.selection_anchor_coordinate = self.cursor_coordinate + elif not select: + self.selection_anchor_coordinate = None + + cursor_row, cursor_column = self.cursor_coordinate + if row is not None: + cursor_row = row + if column is not None: + cursor_column = column + destination = Coordinate(cursor_row, cursor_column) + self.cursor_coordinate = destination + self._scroll_cursor_into_view(animate=animate) + + def _highlight_coordinate(self, coordinate: Coordinate) -> None: + """Apply highlighting to the cell at the coordinate, and post event.""" + self.refresh_coordinate(coordinate) + try: + cell_value = self.get_cell_at(coordinate) + except CellDoesNotExist: + # The cell may not exist e.g. when the table is cleared. + # In that case, there's nothing for us to do here. + return + else: + self.post_message( + DataTable.CellHighlighted(self, cell_value, coordinate=coordinate) + ) + + def _highlight_range( + self, + cursor_coordinate: Coordinate, + selection_anchor_coordinate: Coordinate | None, + ) -> None: + """Apply highlighting to the range at the coordinate, and post event.""" + self.refresh_range(cursor_coordinate, selection_anchor_coordinate) + # TODO: make an event for this. + try: + cell_value = self.get_cell_at(cursor_coordinate) + except CellDoesNotExist: + # The cell may not exist e.g. when the table is cleared. + # In that case, there's nothing for us to do here. + return + else: + self.post_message( + DataTable.CellHighlighted( + self, cell_value, coordinate=cursor_coordinate + ) + ) + + def _highlight_row(self, row_index: int) -> None: + """Apply highlighting to the row at the given index, and post event.""" + self.refresh_row(row_index) + if self.is_valid_row_index(row_index): + self.post_message(DataTable.RowHighlighted(self, row_index)) + + def _highlight_column(self, column_index: int) -> None: + """Apply highlighting to the column at the given index, and post event.""" + self.refresh_column(column_index) + if self.is_valid_column_index(column_index): + self.post_message(DataTable.ColumnHighlighted(self, column_index)) + + def validate_cursor_coordinate(self, value: Coordinate) -> Coordinate: + return self._clamp_cursor_coordinate(value) + + def validate_selection_anchor_coordinate( + self, value: Coordinate | None + ) -> Coordinate | None: + if value is None: + return None + return self._clamp_cursor_coordinate(value) + + def _clamp_cursor_coordinate(self, coordinate: Coordinate) -> Coordinate: + """Clamp a coordinate such that it falls within the boundaries of the table.""" + row, column = coordinate + row = clamp(row, 0, self.row_count - 1) + column = clamp(column, 0, self.column_count - 1) + return Coordinate(row, column) + + def watch_cursor_type(self, old: str, new: str) -> None: + self._set_hover_cursor(False) + if self.show_cursor: + self._highlight_cursor() + + # Refresh cells that were previously impacted by the cursor + # but may no longer be. + if old == "cell": + self.refresh_coordinate(self.cursor_coordinate) + elif old == "range": + self.refresh_range(self.cursor_coordinate, self.selection_anchor_coordinate) + self.selection_anchor_coordinate = None + elif old == "row": + row_index, _ = self.cursor_coordinate + self.refresh_row(row_index) + elif old == "column": + _, column_index = self.cursor_coordinate + self.refresh_column(column_index) + + self._scroll_cursor_into_view() + + def _highlight_cursor(self) -> None: + """Applies the appropriate highlighting and raises the appropriate + [Row|Column|Cell]Highlighted event for the given cursor coordinate + and cursor type.""" + row_index, column_index = self.cursor_coordinate + cursor_type = self.cursor_type + # Apply the highlighting to the newly relevant cells + if cursor_type == "cell": + self._highlight_coordinate(self.cursor_coordinate) + elif cursor_type == "range": + self._highlight_range( + self.cursor_coordinate, self.selection_anchor_coordinate + ) + elif cursor_type == "row": + self._highlight_row(row_index) + elif cursor_type == "column": + self._highlight_column(column_index) + + @property + def _row_label_column_width(self) -> int: + """The render width of the column containing row labels""" + return self._label_column.render_width if self._should_render_row_labels else 0 + + def _update_dimensions(self, new_rows: Iterable[int]) -> None: + """Called to recalculate the virtual (scrollable) size. + + This recomputes column widths and then checks if any of the new rows need + to have their height computed. + + Args: + new_rows: The new rows that will affect the `DataTable` dimensions. + """ + console = self.app.console + auto_height_rows: list[tuple[int, int, list[RenderableType]]] = [] + for row_index in new_rows: + # The row could have been removed before on_idle was called, so we + # need to be quite defensive here and don't assume that the row exists. + if not self.is_valid_row_index(row_index): + continue + + # TODO: support row labels + # row = self.rows.get(row_key) + # assert row is not None + + # if row.label is not None: + # self._labelled_row_exists = True + + row_label, cells_in_row = self._get_row_renderables(row_index) + label_content_width = measure(console, row_label, 1) if row_label else 0 + self._label_column.content_width = max( + self._label_column.content_width, label_content_width + ) + + for column, renderable in zip(self.ordered_columns, cells_in_row): + content_width = measure(console, renderable, 1) + column.content_width = max(column.content_width, content_width) + + # TODO: support row HEIGHT > 1 + # if row.auto_height: + # auto_height_rows.append((row_index, row, cells_in_row)) + + # If there are rows that need to have their height computed, render them + # correctly so that we can cache this rendering for later. + if auto_height_rows: + raise NotImplementedError("todo: support auto-height rows.") + render_cell = self._render_cell # This method renders & caches. + should_highlight = self._should_highlight + cursor_type = self.cursor_type + cursor_location = self.cursor_coordinate + # TODO: handle range selection + hover_location = self.hover_coordinate + base_style = self.rich_style + fixed_style = self.get_component_styles( + "datatable--fixed" + ).rich_style + Style.from_meta({"fixed": True}) + ordered_columns = self.ordered_columns + fixed_columns = self.fixed_columns + + for row_index, row, _ in auto_height_rows: + height = 0 + row_style = self._get_row_style(row_index, base_style) + + # As we go through the cells, save their rendering, height, and + # column width. After we compute the height of the row, go over + # the cells + # that were rendered with the wrong height and append the missing + # padding. + rendered_cells: list[tuple[SegmentLines, int, int]] = [] + for column_index, column in enumerate(ordered_columns): + style = fixed_style if column_index < fixed_columns else row_style + cell_location = Coordinate(row_index, column_index) + rendered_cell = render_cell( + row_index, + column_index, + style, + column.render_width, + cursor=should_highlight( + cursor_location, cell_location, cursor_type + ), + hover=should_highlight( + hover_location, cell_location, cursor_type + ), + # TODO: handle range selection + ) + cell_height = len(rendered_cell) + rendered_cells.append( + (rendered_cell, cell_height, column.render_width) + ) + height = max(height, cell_height) + + row.height = height + # Do surgery on the cache for cells that were rendered with the + # incorrect height during the first pass. + for cell_renderable, cell_height, column_width in rendered_cells: + if cell_height < height: + first_line_space_style = cell_renderable[0][0].style + cell_renderable.extend( + [ + [Segment(" " * column_width, first_line_space_style)] + for _ in range(height - cell_height) + ] + ) + + data_cells_width = sum(column.render_width for column in self.ordered_columns) + total_width = data_cells_width + self._row_label_column_width + header_height = self.header_height if self.show_header else 0 + self.virtual_size = Size( + total_width, + self._total_row_height + header_height, + ) + + def _get_cell_region(self, coordinate: Coordinate) -> Region: + """Get the region of the cell at the given spatial coordinate.""" + if not self.is_valid_coordinate(coordinate): + return Region(0, 0, 0, 0) + + row_index, column_index = coordinate + + # The x-coordinate of a cell is the sum of widths of the data cells to the left + # plus the width of the render width of the longest row label. + x = ( + sum(column.render_width for column in self.ordered_columns[:column_index]) + # TODO: support row labels + # + self._row_label_column_width + ) + width = self.ordered_columns[column_index].render_width + height = 1 # row.height + # TODO: support multiple heights + # y = sum(ordered_row.height for ordered_row in self.ordered_rows[:row_index]) + y = row_index + if self.show_header: + y += self.header_height + cell_region = Region(x, y, width, height) + return cell_region + + def _get_range_region(self, coordinate: Coordinate, anchor: Coordinate) -> Region: + """ + Get the region of the range defined by the box with corners at anchor + and coordinate. + """ + if not self.is_valid_coordinate(coordinate) or not self.is_valid_coordinate( + anchor + ): + return Region(0, 0, 0, 0) + + min_row, max_row, min_col, max_col = self._order_bounding_coords( + coordinate, anchor + ) + + x = ( + sum(column.render_width for column in self.ordered_columns[:min_col]) + # TODO: support row labels + # + self._row_label_column_width + ) + width = sum( + column.render_width + for column in self.ordered_columns[min_col : max_col + 1] + ) + y = min_row + if self.show_header: + y += self.header_height + + # TODO: support variable-height rows + height = 1 * (max_row - min_row + 1) + return Region(x=x, y=y, width=width, height=height) + + def _get_row_region(self, row_index: int) -> Region: + """Get the region of the row at the given index.""" + if not self.is_valid_row_index(row_index): + return Region(0, 0, 0, 0) + + row_width = ( + sum(column.render_width for column in self.ordered_columns) + # TODO: support row labels + # + self._row_label_column_width + ) + # TODO: support multiple heights + # y = sum(ordered_row.height for ordered_row in self.ordered_rows[:row_index]) + y = row_index + if self.show_header: + y += self.header_height + row_region = Region(0, y, row_width, 1) # row.height) + return row_region + + def _get_column_region(self, column_index: int) -> Region: + """Get the region of the column at the given index.""" + if not self.is_valid_column_index(column_index): + return Region(0, 0, 0, 0) + + x = ( + sum(column.render_width for column in self.ordered_columns[:column_index]) + + self._row_label_column_width + ) + width = self.ordered_columns[column_index].render_width + header_height = self.header_height if self.show_header else 0 + height = self._total_row_height + header_height + full_column_region = Region(x, 0, width, height) + return full_column_region + + def clear(self, columns: bool = False) -> Self: + """Clear the table. + + Args: + columns: Also clear the columns. + + Returns: + The `DataTable` instance. + """ + # TODO: make Backend optional and reactive? + raise NotImplementedError("Unmount this table and mount a new one instead.") + self._clear_caches() + self._y_offsets.clear() + self._data.clear() + self.rows.clear() + self._row_locations = TwoWayDict({}) + if columns: + self.columns.clear() + self._column_locations = TwoWayDict({}) + self._require_update_dimensions = True + self.cursor_coordinate = Coordinate(0, 0) + self.selection_anchor_coordinate = None + self.hover_coordinate = Coordinate(0, 0) + self._label_column = Column(self._label_column_key, Text(), auto_width=True) + self._labelled_row_exists = False + self.refresh() + self.scroll_x = 0 + self.scroll_y = 0 + self.scroll_target_x = 0 + self.scroll_target_y = 0 + return self + + def add_column( + self, + label: TextType, + *, + width: int | None = None, + default: CellType | None = None, + ) -> int: + """Add a column to the table. + + Args: + label: A str or Text object containing the label (shown top of column). + width: Width of the column in cells or None to fit content. + key: A key which uniquely identifies this column. + If None, it will be generated for you. + default: The value to insert into pre-existing rows. + + Returns: + Uniquely identifies this column. Can be used to retrieve this column + regardless of its current location in the DataTable (it could have moved + after being added due to sorting/insertion/deletion of other columns). + """ + label = Text.from_markup(label) if isinstance(label, str) else label + label_width = measure(self.app.console, label, 1) + if width is None: + col = Column( + label, + label_width, + content_width=label_width, + auto_width=True, + ) + else: + col = Column( + label, + width, + content_width=label_width, + ) + if self._ordered_columns is not None: + self._ordered_columns.append(col) + elif self._column_labels is not None: + self._column_labels.append(label) + + if self._column_widths is not None: + self._column_widths.append(width) + + # Update backend to account for the new column. + if self.backend is not None: + column_index = self.backend.append_column(str(label), default=default) + elif self._column_labels is not None: + column_index = len(self._column_labels) + else: + column_index = 0 + + self._require_update_dimensions = True + self.check_idle() + + return column_index + + def add_row( + self, + *cells: CellType, + height: int | None = 1, + label: TextType | None = None, + ) -> int: + """Add a row at the bottom of the DataTable. + + Args: + *cells: Positional arguments should contain cell data. + height: The height of a row (in lines). Use `None` to auto-detect the + optimal height. + key: A key which uniquely identifies this row. If None, it will be + generated for you and returned. + label: The label for the row. Will be displayed to the left if supplied. + + Returns: + Unique identifier for this row. Can be used to retrieve this row regardless + of its current location in the DataTable (it could have moved after + being added due to sorting or insertion/deletion of other rows). + """ + if label is not None: + raise NotImplementedError("todo: support labeled rows") + elif height != 1: + raise NotImplementedError("todo: support auto-height rows") + + [index] = self.add_rows([cells]) + return index + + def add_columns(self, *labels: TextType) -> list[int]: + """Add a number of columns. + + Args: + *labels: Column headers. + + Returns: + A list of the keys for the columns that were added. See + the `add_column` method docstring for more information on how + these keys are used. + """ + column_indexes = [] + for label in labels: + column_index = self.add_column(label, width=None) + column_indexes.append(column_index) + return column_indexes + + def add_rows(self, rows: Iterable[Iterable[Any]]) -> list[int]: + """Add a number of rows at the bottom of the DataTable. + + Args: + rows: Iterable of rows. A row is an iterable of cells. + + Returns: + A list of the keys for the rows that were added. See + the `add_row` method docstring for more information on how + these keys are used. + """ + if self.backend is None: + self.backend = create_backend(list(rows)) + indicies = list(range(self.row_count)) + else: + indicies = self.backend.append_rows(rows) + self._require_update_dimensions = True + self.cursor_coordinate = self.cursor_coordinate + + # If a position has opened for the cursor to appear, where it previously + # could not (e.g. when there's no data in the table), then a highlighted + # event is posted, since there's now a highlighted cell when there wasn't + # before. + cell_now_available = self.row_count == 1 and len(self.ordered_columns) > 0 + visible_cursor = self.show_cursor and self.cursor_type != "none" + if cell_now_available and visible_cursor: + self._highlight_cursor() + + self._update_count += 1 + self.check_idle() + return indicies + + def remove_row(self, row_index: int) -> None: + """Remove a row (identified by a key) from the DataTable. + + Args: + row_key: The key identifying the row to remove. + + Raises: + RowDoesNotExist: If the row key does not exist. + """ + if self.backend is None: + raise RowDoesNotExist("No data in the table") + self.backend.drop_row(row_index) + + self.cursor_coordinate = self.cursor_coordinate + self.selection_anchor_coordinate = self.selection_anchor_coordinate + self.hover_coordinate = self.hover_coordinate + + self._update_count += 1 + self._require_update_dimensions = True + self.refresh(layout=True) + self.check_idle() + + # def remove_column(self, column_index: int) -> None: + # """Remove a column (identified by a key) from the DataTable. + + # Args: + # column_key: The key identifying the column to remove. + + # Raises: + # ColumnDoesNotExist: If the column key does not exist. + # """ + # raise NotImplementedError("No updates allowed.") + # if column_key not in self._column_locations: + # raise ColumnDoesNotExist(f"Column key {column_key!r} is not valid.") + + # self._require_update_dimensions = True + # self.check_idle() + + # index_to_delete = self._column_locations.get(column_key) + # new_column_locations = TwoWayDict({}) + # for column_location_key in self._column_locations: + # column_index = self._column_locations.get(column_location_key) + # if column_index > index_to_delete: + # new_column_locations[column_location_key] = column_index - 1 + # elif column_index < index_to_delete: + # new_column_locations[column_location_key] = column_index + + # self._column_locations = new_column_locations + + # del self.columns[column_key] + # for row in self._data: + # del self._data[row][column_key] + + # self.cursor_coordinate = self.cursor_coordinate + # self.selection_anchor_coordinate = self.selection_anchor_coordinate + # self.hover_coordinate = self.hover_coordinate + + # self._update_count += 1 + # self.refresh(layout=True) + + async def _on_idle(self, _: events.Idle) -> None: + """Runs when the message pump is empty. + + We use this for some expensive calculations like re-computing dimensions of the + whole DataTable and re-computing column widths after some cells + have been updated. This is more efficient in the case of high + frequency updates, ensuring we only do expensive computations once.""" + # TODO: allow updates + pass + # if self._updated_cells: + # Cell contents have already been updated at this point. + # Now we only need to worry about measuring column widths. + # updated_columns = self._updated_cells.copy() + # self._updated_cells.clear() + # self._update_column_widths(updated_columns) + + if self._require_update_dimensions: + # Add the new rows *before* updating the column widths, since + # cells in a new row may influence the final width of a column. + # Only then can we compute optimal height of rows with "auto" height. + self._require_update_dimensions = False + # new_rows = self._new_rows.copy() + new_rows: list[int] = [] + # self._new_rows.clear() + self._update_dimensions(new_rows) + + def refresh_coordinate(self, coordinate: Coordinate) -> Self: + """Refresh the cell at a coordinate. + + Args: + coordinate: The coordinate to refresh. + + Returns: + The `DataTable` instance. + """ + if not self.is_valid_coordinate(coordinate): + return self + region = self._get_cell_region(coordinate) + self._refresh_region(region) + return self + + def refresh_range(self, coordinate: Coordinate, anchor: Coordinate | None) -> Self: + """Refresh the cell at a coordinate. + + Args: + coordinate: The coordinate to refresh. + + Returns: + The `DataTable` instance. + """ + if anchor is None: + return self.refresh_coordinate(coordinate) + if not self.is_valid_coordinate(coordinate): + return self + region = self._get_range_region(coordinate, anchor) + self._refresh_region(region) + return self + + def refresh_row(self, row_index: int) -> Self: + """Refresh the row at the given index. + + Args: + row_index: The index of the row to refresh. + + Returns: + The `DataTable` instance. + """ + if not self.is_valid_row_index(row_index): + return self + + region = self._get_row_region(row_index) + self._refresh_region(region) + return self + + def refresh_column(self, column_index: int) -> Self: + """Refresh the column at the given index. + + Args: + column_index: The index of the column to refresh. + + Returns: + The `DataTable` instance. + """ + if not self.is_valid_column_index(column_index): + return self + + region = self._get_column_region(column_index) + self._refresh_region(region) + return self + + def _refresh_region(self, region: Region) -> Self: + """Refresh a region of the DataTable, if it's visible within the window. + + This method will translate the region to account for scrolling. + + Returns: + The `DataTable` instance. + """ + if not self.window_region.overlaps(region): + return self + region = region.translate(-self.scroll_offset) + self.refresh(region) + return self + + def is_valid_row_index(self, row_index: int) -> bool: + """Return a boolean indicating whether the row_index is within table bounds. + + Args: + row_index: The row index to check. + + Returns: + True if the row index is within the bounds of the table. + """ + return 0 <= row_index < self.row_count + + def is_valid_column_index(self, column_index: int) -> bool: + """Return a boolean indicating whether the column_index is within table bounds. + + Args: + column_index: The column index to check. + + Returns: + True if the column index is within the bounds of the table. + """ + return 0 <= column_index < self.column_count + + def is_valid_coordinate(self, coordinate: Coordinate) -> bool: + """Return a boolean indicating whether the given coordinate is valid. + + Args: + coordinate: The coordinate to validate. + + Returns: + True if the coordinate is within the bounds of the table. + """ + row_index, column_index = coordinate + return self.is_valid_row_index(row_index) and self.is_valid_column_index( + column_index + ) + + @property + def ordered_columns(self) -> list[Column]: + """The list of Columns in the DataTable, ordered as they appear on screen.""" + if self._column_labels is not None: + labels = self._column_labels + elif self.backend is not None: + labels = list(self.backend.columns) + else: + labels = [] + + if self._column_widths is not None: + widths = self._column_widths + else: + widths = [0] * len(labels) + + if self.backend is not None: + column_content_widths = self.backend.column_content_widths + else: + column_content_widths = [0] * len(labels) + + if self._ordered_columns is None: + self._ordered_columns = [ + Column( + label=Text.from_markup(label) if isinstance(label, str) else label, + width=width if width is not None else 0, + content_width=content_width, + auto_width=True if width is None or width == 0 else False, + max_content_width=self.max_column_content_width, + ) + for label, width, content_width in zip( + labels, widths, column_content_widths + ) + ] + return self._ordered_columns + + # @property + # def ordered_rows(self) -> list[Row]: + # """The list of Rows in the DataTable, ordered as they appear on screen.""" + # raise NotImplementedError("Unused and unwise.") + # num_rows = self.row_count + # update_count = self._update_count + # cache_key = (num_rows, update_count) + # if cache_key in self._ordered_row_cache: + # ordered_rows = self._ordered_row_cache[cache_key] + # else: + # row_indices = range(num_rows) + # ordered_rows = [] + # for row_index in row_indices: + # row_key = self._row_locations.get_key(row_index) + # row = self.rows[row_key] + # ordered_rows.append(row) + # self._ordered_row_cache[cache_key] = ordered_rows + # return ordered_rows + + @property + def _should_render_row_labels(self) -> bool: + """Whether row labels should be rendered or not.""" + return self._labelled_row_exists and self.show_row_labels + + def _get_row_renderables(self, row_index: int) -> RowRenderables: + """Get renderables for the row currently at the given row index. The renderables + returned here have already been passed through the default_cell_formatter. + + Args: + row_index: Index of the row. + + Returns: + A RowRenderables containing the optional label and the rendered cells. + """ + ordered_columns = self.ordered_columns + if row_index == -1: + header_row: list[RenderableType] = [ + # TODO: make this pluggable so we can override the native labels + column.label + for column in ordered_columns + ] + # This is the cell where header and row labels intersect + return RowRenderables(None, header_row) + + ordered_row = self.get_row_at(row_index) + empty = self.null_rep + + formatted_row_cells = [ + cell_formatter( + datum, null_rep=empty, col=col, render_markup=self.render_markup + ) + for datum, col in zip_longest(ordered_row, self.ordered_columns) + ] + label = None + if self._should_render_row_labels: + raise NotImplementedError("Todo: support row labels") + # row_metadata = self.rows.get(self._row_locations.get_key(row_index)) + # label = ( + # default_cell_formatter(row_metadata.label) + # if row_metadata.label + # else None + # ) + return RowRenderables(label, formatted_row_cells) + + def _get_cell_renderable( + self, row_index: int, column_index: int + ) -> RenderableType | Text: + """Get renderables for the cell currently at the given row index, + column index tuple. The renderable + returned here has already been passed through the default_cell_formatter. + + Args: + row_index: Index of the row. + column_index: Index of the column. + + Returns: + A RenderableType (or Text) containing the the rendered cell. + """ + if row_index == -1: + return self.ordered_columns[column_index].label + + datum = self.get_cell_at(Coordinate(row=row_index, column=column_index)) + return cell_formatter( + datum, + null_rep=self.null_rep, + col=self.ordered_columns[column_index], + render_markup=self.render_markup, + ) + + def _render_cell( + self, + row_index: int, + column_index: int, + base_style: Style, + width: int, + cursor: bool = False, + hover: bool = False, + in_range: bool = False, + ) -> SegmentLines: + """Render the given cell. + + Args: + row_index: Index of the row. + column_index: Index of the column. + base_style: Style to apply. + width: Width of the cell. + cursor: Is this cell affected by cursor highlighting? + hover: Is this cell affected by hover cursor highlighting? + in_range: is this cell affected by selection range highlighting? + + Returns: + A list of segments per line. + """ + is_header_cell = row_index == -1 + is_row_label_cell = column_index == -1 + + is_fixed_style_cell = ( + not is_header_cell + and not is_row_label_cell + and (row_index < self.fixed_rows or column_index < self.fixed_columns) + ) + + cell_cache_key: CellCacheKey = ( + row_index, + column_index, + base_style, + cursor, + hover, + in_range, + self._show_hover_cursor, + self._update_count, + self._pseudo_class_state, + ) + + if cell_cache_key not in self._cell_render_cache: + base_style += Style.from_meta({"row": row_index, "column": column_index}) + + if is_row_label_cell: + row_label, _ = self._get_row_renderables(row_index) + cell = row_label if row_label is not None else "" + else: + cell = self._get_cell_renderable( + row_index=row_index, column_index=column_index + ) + + component_style, post_style = self._get_styles_to_render_cell( + is_header_cell, + is_row_label_cell, + is_fixed_style_cell, + hover, + cursor, + in_range, + self.show_cursor, + self._show_hover_cursor, + self.cursor_type == "range", + self.cursor_foreground_priority == "css", + self.cursor_background_priority == "css", + ) + + if is_header_cell: + options = self.app.console.options.update_dimensions( + width, self.header_height + ) + else: + # TODO: support rows with height > 1 + # row = self.rows[row_key] + # If an auto-height row hasn't had its height calculated, we don't fix + # the value for `height` so that we can measure the height of the cell. + # if row.auto_height and row.height == 0: + # options = self.app.console.options.update_width(width) + # else: + options = self.app.console.options.update_dimensions(width, 1) + if self.max_column_content_width is not None: + options.overflow = "ellipsis" + options.no_wrap = True + lines = self.app.console.render_lines( + Styled( + Padding(cell, (0, 1)), + pre_style=base_style + component_style, + post_style=post_style, + ), + options, + ) + + self._cell_render_cache[cell_cache_key] = lines + + return self._cell_render_cache[cell_cache_key] + + @functools.lru_cache(maxsize=32) # noqa B019 + def _get_styles_to_render_cell( + self, + is_header_cell: bool, + is_row_label_cell: bool, + is_fixed_style_cell: bool, + hover: bool, + cursor: bool, + in_range: bool, + show_cursor: bool, + show_hover_cursor: bool, + show_range_highlight: bool, + has_css_foreground_priority: bool, + has_css_background_priority: bool, + ) -> tuple[Style, Style]: + """Auxiliary method to compute styles used to render a given cell. + + Args: + is_header_cell: Is this a cell from a header? + is_row_label_cell: Is this the label of any given row? + is_fixed_style_cell: Should this cell be styled like a fixed cell? + hover: Does this cell have the hover pseudo class? + cursor: Is this cell covered by the cursor? + in_range: Is this cell in the selection range? + show_cursor: Do we want to show the cursor in the data table? + show_hover_cursor: Do we want to show the mouse hover when using + the keyboard to move the cursor? + has_css_foreground_priority: `self.cursor_foreground_priority == "css"`? + has_css_background_priority: `self.cursor_background_priority == "css"`? + """ + get_component = self.get_component_rich_style + component_style = Style() + + if hover and show_cursor and show_hover_cursor: + component_style += get_component("datatable--hover") + if is_header_cell or is_row_label_cell: + # Apply subtle variation in style for the header/label (blue + # background by default) rows and columns affected by the cursor, to + # ensure we can still differentiate between the labels and the data. + component_style += get_component("datatable--header-hover") + + if cursor and show_cursor: + cursor_style = get_component("datatable--cursor") + component_style += cursor_style + if is_header_cell or is_row_label_cell: + component_style += get_component("datatable--header-cursor") + elif is_fixed_style_cell: + component_style += get_component("datatable--fixed-cursor") + elif in_range and show_range_highlight and show_cursor: + cursor_style = get_component("datatable--selectrange") + component_style += cursor_style + if is_header_cell or is_row_label_cell: + component_style += get_component("datatable--header-selectrange") + + post_foreground = ( + Style.from_color(color=component_style.color) + if has_css_foreground_priority + else Style.null() + ) + post_background = ( + Style.from_color(bgcolor=component_style.bgcolor) + if has_css_background_priority + else Style.null() + ) + + return component_style, post_foreground + post_background + + def _render_line_in_row( + self, + row_index: int, + line_no: int, + x1: int, + x2: int, + base_style: Style, + cursor_location: Coordinate, + hover_location: Coordinate, + selection_anchor_location: Coordinate | None, + ) -> tuple[SegmentLines, SegmentLines, int, int]: + """Render a single line from a row in the DataTable. + + Args: + row_key: The identifying key for this row. + line_no: Line number (y-coordinate) within row. 0 is the first strip of + cells in the row, line_no=1 is the next line in the row, and so on... + x1: X start crop. + x2: X end crop (exclusive). + base_style: Base style of row. + cursor_location: The location of the cursor in the DataTable. + hover_location: The location of the hover cursor in the DataTable. + selection_anchor_location: The location of the selection anchor, or None. + + Returns: + Lines for fixed cells, and Lines for scrollable cells, and the x-offset + for the first cell in scrollable cells. + """ + cursor_type = self.cursor_type + show_cursor = self.show_cursor + + # determine which columns are visible (col2 inclusive) + fixed_width = sum( + [col.render_width for col in self.ordered_columns[: self.fixed_columns]] + ) + col_widths = [ + col.render_width for col in self.ordered_columns[self.fixed_columns :] + ] + cumulative_width = 0 + hidden_width = 0 + visible_width = self.size.width - fixed_width + col1 = self.fixed_columns + col2 = None + for i, w in enumerate(col_widths, start=self.fixed_columns): + if cumulative_width < x1: + col1 = i + hidden_width = cumulative_width + if col2 is None and cumulative_width - x1 > visible_width: + col2 = i + break + cumulative_width += w + else: + col2 = len(self.ordered_columns) - 1 + + cache_key = ( + row_index, + line_no, + col1, + col2, + base_style, + cursor_location, + hover_location, + selection_anchor_location, + cursor_type, + show_cursor, + self._show_hover_cursor, + self._update_count, + self._pseudo_class_state, + ) + + if cache_key in self._row_render_cache: + cache_contents = self._row_render_cache[cache_key] + return cache_contents[0], cache_contents[1], hidden_width, fixed_width + + should_highlight = self._should_highlight + should_highlight_range = self._should_highlight_range + render_cell = self._render_cell + header_style = self.get_component_styles("datatable--header").rich_style + + if not self.is_valid_row_index(row_index): + row_index = -1 + + # If the row has a label, add it to fixed_row here with correct style. + fixed_row = [] + + if self._labelled_row_exists and self.show_row_labels: + # The width of the row label is updated again on idle + cell_location = Coordinate(row_index, -1) + label_cell_lines = render_cell( + row_index, + -1, + header_style, + width=self._row_label_column_width, + cursor=should_highlight(cursor_location, cell_location, cursor_type), + hover=should_highlight(hover_location, cell_location, cursor_type), + in_range=should_highlight_range( + cursor_location, + selection_anchor_location, + cell_location, + cursor_type, + ), + )[line_no] + fixed_row.append(label_cell_lines) + + if self.fixed_columns: + if row_index == self._header_row_index: + fixed_style = header_style # We use the header style either way. + else: + fixed_style = self.get_component_styles("datatable--fixed").rich_style + fixed_style += Style.from_meta({"fixed": True}) + for column_index, column in enumerate( + self.ordered_columns[: self.fixed_columns] + ): + cell_location = Coordinate(row_index, column_index) + fixed_cell_lines = render_cell( + row_index, + column_index, + fixed_style, + column.render_width, + cursor=should_highlight( + cursor_location, cell_location, cursor_type + ), + hover=should_highlight(hover_location, cell_location, cursor_type), + in_range=should_highlight_range( + cursor_location, + selection_anchor_location, + cell_location, + cursor_type, + ), + )[line_no] + fixed_row.append(fixed_cell_lines) + + row_style = self._get_row_style(row_index, base_style) + + scrollable_row = [] + visible_columns = self.ordered_columns[col1 : col2 + 1] + for column_index, column in enumerate(visible_columns, start=col1): + cell_location = Coordinate(row_index, column_index) + cell_lines = render_cell( + row_index, + column_index, + row_style, + column.render_width, + cursor=should_highlight(cursor_location, cell_location, cursor_type), + hover=should_highlight(hover_location, cell_location, cursor_type), + in_range=should_highlight_range( + cursor_location, + selection_anchor_location, + cell_location, + cursor_type, + ), + )[line_no] + scrollable_row.append(cell_lines) + + # Extending the styling out horizontally to fill the container + widget_width = self.size.width + table_width = ( + sum(col_widths[self.fixed_columns :]) + self._row_label_column_width + ) + remaining_space = max(0, widget_width - table_width) + background_color = self.background_colors[1] + faded_color = Color.from_rich_color(row_style.bgcolor).blend( + background_color, factor=0.25 + ) + faded_style = Style.from_color( + color=row_style.color, bgcolor=faded_color.rich_color + ) + scrollable_row.append([Segment(" " * remaining_space, faded_style)]) + + self._row_render_cache[cache_key] = (fixed_row, scrollable_row) + return (fixed_row, scrollable_row, hidden_width, fixed_width) + + def _get_offsets(self, y: int) -> tuple[int, int]: + """Get row key and line offset for a given line. + + Args: + y: Y coordinate relative to DataTable top. + + Returns: + Row key and line (y) offset within cell. + """ + raise NotImplementedError("todo: support heights > 1") + header_height = self.header_height + y_offsets = self._y_offsets + if self.show_header: + if y < header_height: + return self._header_row_key, y + y -= header_height + if y > len(y_offsets): + raise LookupError("Y coord {y!r} is greater than total height") + + return y_offsets[y] + + def _render_line(self, y: int, x1: int, x2: int, base_style: Style) -> Strip: + """Render a (possibly cropped) line in to a Strip (a list of segments + representing a horizontal line). + + Args: + y: Y coordinate of line + x1: X start crop. + x2: X end crop (exclusive). + base_style: Style to apply to line. + + Returns: + The Strip which represents this cropped line. + """ + + width = self.size.width + + # todo: support rows with height > 1 + # try: + # row_key, y_offset_in_row = self._get_offsets(y) + # except LookupError: + # return Strip.blank(width, base_style) + row_index = y - 1 + if not self.is_valid_row_index(row_index) and row_index != -1: + return Strip.blank(width, base_style) + + cache_key = ( + y, + x1, + x2, + width, + self.cursor_coordinate, + self.hover_coordinate, + self.selection_anchor_coordinate, + base_style, + self.cursor_type, + self._show_hover_cursor, + self._update_count, + self._pseudo_class_state, + ) + if cache_key in self._line_cache: + return self._line_cache[cache_key] + + fixed, scrollable, hidden_width, fixed_width = self._render_line_in_row( + row_index, + 0, + x1, + x2, + base_style, + cursor_location=self.cursor_coordinate, + hover_location=self.hover_coordinate, + selection_anchor_location=self.selection_anchor_coordinate, + ) + + fixed_line: list[Segment] = list(chain.from_iterable(fixed)) if fixed else [] + scrollable_line: list[Segment] = list(chain.from_iterable(scrollable)) + + segments = fixed_line + line_crop( + scrollable_line, x1 - hidden_width, x2 - hidden_width, width - fixed_width + ) + strip = Strip(segments).adjust_cell_length(width, base_style).simplify() + + self._line_cache[cache_key] = strip + return strip + + def render_lines(self, crop: Region) -> list[Strip]: + self._pseudo_class_state = self.get_pseudo_class_state() + return super().render_lines(crop) + + def render_line(self, y: int) -> Strip: + width, _ = self.size + scroll_x, scroll_y = self.scroll_offset + + fixed_rows_height = self.fixed_rows + # sum( + # self.get_row_height(row_key) for row_key in fixed_row_keys + # ) + if self.show_header: + fixed_rows_height += self.header_height + + if y >= fixed_rows_height: + y += scroll_y + + return self._render_line(y, scroll_x, scroll_x + width, self.rich_style) + + def _should_highlight( + self, + cursor: Coordinate, + target_cell: Coordinate, + type_of_cursor: CursorType, + ) -> bool: + """Determine if the given cell should be highlighted because of the cursor. + + This auxiliary method takes the cursor position and type into account when + determining whether the cell should be highlighted. + + Args: + cursor: The current position of the cursor. + target_cell: The cell we're checking for the need to highlight. + type_of_cursor: The type of cursor that is currently active. + + Returns: + Whether or not the given cell should be highlighted. + """ + if type_of_cursor in ("cell", "range"): + return cursor == target_cell + elif type_of_cursor == "row": + cursor_row, _ = cursor + cell_row, _ = target_cell + return cursor_row == cell_row + elif type_of_cursor == "column": + _, cursor_column = cursor + _, cell_column = target_cell + return cursor_column == cell_column + else: + return False + + def _should_highlight_range( + self, + cursor: Coordinate, + selection_anchor: Coordinate | None, + target_cell: Coordinate, + type_of_cursor: CursorType, + ) -> bool: + if type_of_cursor != "range": + return False + elif selection_anchor is None: + return False + min_row, max_row, min_col, max_col = self._order_bounding_coords( + cursor, selection_anchor + ) + if (min_row <= target_cell.row <= max_row) and ( + min_col <= target_cell.column <= max_col + ): + return True + else: + return False + + def _get_row_style(self, row_index: int, base_style: Style) -> Style: + """Gets the Style that should be applied to the row at the given index. + + Args: + row_index: The index of the row to style. + base_style: The base style to use by default. + + Returns: + The appropriate style. + """ + + if row_index == -1: + row_style = self.get_component_styles("datatable--header").rich_style + elif row_index < self.fixed_rows: + row_style = self.get_component_styles("datatable--fixed").rich_style + else: + if self.zebra_stripes: + component_row_style = ( + "datatable--odd-row" if row_index % 2 else "datatable--even-row" + ) + row_style = self.get_component_styles(component_row_style).rich_style + else: + row_style = base_style + return row_style + + def _on_leave(self, _: events.Leave) -> None: + self._set_hover_cursor(False) + self.tooltip = None + + def _get_fixed_offset(self) -> Spacing: + """Calculate the "fixed offset", that is the space to the top and left + that is occupied by fixed rows and columns respectively. Fixed rows and columns + are rows and columns that do not participate in scrolling.""" + top = self.header_height if self.show_header else 0 + # TODO: Support row heights > 1 + # top += sum(row.height for row in self.ordered_rows[: self.fixed_rows]) + top += self.fixed_rows + left = ( + sum( + column.render_width + for column in self.ordered_columns[: self.fixed_columns] + ) + + self._row_label_column_width + ) + return Spacing(top, 0, 0, left) + + def sort( + self, + by: list[tuple[str, Literal["ascending", "descending"]]] | str, + ) -> Self: + """Sort the rows in the `DataTable` by one or more column keys. + + Args: + by: str sorts table by the data in the column with that name (asc). + by: list[tuple] sorts the table by the named column(s) with the + directions indicated. + + Returns: + The `DataTable` instance. + """ + if self.backend is None: + return self + self.backend.sort(by=by) + self._update_count += 1 + self.refresh() + return self + + def _scroll_cursor_into_view(self, animate: bool = False) -> None: + """When the cursor is at a boundary of the DataTable and moves out + of view, this method handles scrolling to ensure it remains visible.""" + fixed_offset = self._get_fixed_offset() + top, _, _, left = fixed_offset + + if self.cursor_type == "row": + x, y, width, height = self._get_row_region(self.cursor_row) + region = Region(int(self.scroll_x) + left, y, width - left, height) + elif self.cursor_type == "column": + x, y, width, height = self._get_column_region(self.cursor_column) + region = Region(x, int(self.scroll_y) + top, width, height - top) + else: + region = self._get_cell_region(self.cursor_coordinate) + + self.scroll_to_region(region, animate=animate, spacing=fixed_offset) + + def _set_hover_cursor(self, active: bool) -> None: + """Set whether the hover cursor (the faint cursor you see when you + hover the mouse cursor over a cell) is visible or not. Typically, + when you interact with the keyboard, you want to switch the hover + cursor off. + + Args: + active: Display the hover cursor. + """ + self._show_hover_cursor = active + cursor_type = self.cursor_type + if cursor_type == "column": + self.refresh_column(self.hover_column) + elif cursor_type == "row": + self.refresh_row(self.hover_row) + elif cursor_type in ("cell", "range"): + self.refresh_coordinate(self.hover_coordinate) + + @on(events.MouseDown) + async def move_cursor_to_mouse_down(self, event: events.MouseDown) -> None: + self._set_hover_cursor(True) + meta = event.style.meta + if not meta: + return + + row_index = meta["row"] + column_index = meta["column"] + is_header_click = self.show_header and row_index == -1 + is_row_label_click = self.show_row_labels and column_index == -1 + if is_header_click: + # Header clicks work even if cursor is off, and doesn't move the cursor. + column = self.ordered_columns[column_index] + message = DataTable.HeaderSelected(self, column_index, label=column.label) + self.post_message(message) + elif is_row_label_click: + # TODO: support row labels. + # row = self.ordered_rows[row_index] + row_message = DataTable.RowLabelSelected( + self, + row_index, + label=Text(), # label=row.label + ) + self.post_message(row_message) + elif self.show_cursor and self.cursor_type != "none": + # Only post selection events if there is a visible row/col/cell cursor. + self.selection_anchor_coordinate = None + self.cursor_coordinate = Coordinate(row_index, column_index) + self._scroll_cursor_into_view(animate=True) + event.stop() + + @on(events.MouseUp) + async def move_cursor_to_mouse_up(self, event: events.MouseUp) -> None: + self._set_hover_cursor(True) + meta = event.style.meta + if not meta: + return + + row_index = meta["row"] + column_index = meta["column"] + click_coordinate = Coordinate(row_index, column_index) + is_header_click = self.show_header and row_index == -1 + # is_row_label_click = self.show_row_labels and column_index == -1 + if is_header_click: + # don't move the cursor + pass + # elif is_row_label_click: + # # TODO: support row labels. + # # row = self.ordered_rows[row_index] + # row_message = DataTable.RowLabelSelected( + # self, row_index, label=Text() # label=row.label + # ) + # self.post_message(row_message) + elif ( + self.show_cursor + and self.cursor_type == "range" + and click_coordinate != self.cursor_coordinate + ): + # Only post selection events if there is a visible row/col/cell cursor. + if self.selection_anchor_coordinate is None: + self.selection_anchor_coordinate = self.cursor_coordinate + self.cursor_coordinate = click_coordinate + self._post_selected_message() + self._scroll_cursor_into_view(animate=True) + event.stop() + + @on(events.MouseMove) + def set_hover_or_cursor_from_mouse(self, event: events.MouseMove) -> None: + """If the hover cursor is visible, display it by extracting the row + and column metadata from the segments present in the cells.""" + self.tooltip = None + meta = event.style.meta + if not meta: + self._set_hover_cursor(False) + return + else: + self._set_hover_cursor(True) + + mouse_coordinate = Coordinate(meta["row"], meta["column"]) + if event.button == 1 and self.cursor_type == "range": # left click and drag + if self.selection_anchor_coordinate is None: + self.selection_anchor_coordinate = self.cursor_coordinate + self.cursor_coordinate = mouse_coordinate + self._scroll_cursor_into_view(animate=True) + event.stop() + elif self.show_cursor and self.cursor_type != "none": + try: + self.hover_coordinate = mouse_coordinate + except KeyError: + pass + + self._set_tooltip_from_cell_at(mouse_coordinate) + + def _set_tooltip_from_cell_at(self, coordinate: Coordinate) -> None: + # TODO: support row labels + cache_key = (coordinate.row, coordinate.column, self._update_count) + column = self.ordered_columns[coordinate.column] + if cache_key not in self._tooltip_cache: + if coordinate.row == -1: # hover over header + raw_value = column.label + else: + raw_value = self.get_cell_at(coordinate) + if raw_value is None: + raw_value = self.null_rep + measured_width = measure_width(raw_value, self.app.console) + if ( + self.max_column_content_width is not None + and measured_width > self.max_column_content_width + ) or (measured_width > column.render_width): + if isinstance(raw_value, Text): + self._tooltip_cache[cache_key] = raw_value + elif isinstance( + raw_value, + (str, float, Decimal, int, datetime, time, date, timedelta), + ): + self._tooltip_cache[cache_key] = cell_formatter( + raw_value, null_rep=self.null_rep, col=column + ) + else: + self._tooltip_cache[cache_key] = Pretty(raw_value) + else: + self._tooltip_cache[cache_key] = None + self.tooltip = self._tooltip_cache[cache_key] + + def _set_selection_anchor(self, select: bool) -> None: + if ( + select + and self.cursor_type == "range" + and self.selection_anchor_coordinate is None + ): + self.selection_anchor_coordinate = self.cursor_coordinate + elif not select: + self.selection_anchor_coordinate = None + + def action_page_down(self, select: bool = False) -> None: + """Move the cursor one page down.""" + self._set_hover_cursor(False) + if self.show_cursor and self.cursor_type in ("cell", "row", "range"): + height = self.size.height - (self.header_height if self.show_header else 0) + + # Determine how many rows constitutes a "page" + rows_to_scroll = 0 + row_index, column_index = self.cursor_coordinate + self._set_selection_anchor(select) + # TODO: support rows with height > 1 + # for ordered_row in self.ordered_rows[row_index:]: + # offset += ordered_row.height + # if offset > height: + # break + # rows_to_scroll += 1 + rows_to_scroll = height + + self.cursor_coordinate = Coordinate( + row_index + rows_to_scroll - 1, column_index + ) + else: + super().action_page_down() + + def action_page_up(self, select: bool = False) -> None: + """Move the cursor one page up.""" + self._set_hover_cursor(False) + if self.show_cursor and self.cursor_type in ("cell", "row", "range"): + height = self.size.height - (self.header_height if self.show_header else 0) + + # Determine how many rows constitutes a "page" + row_index, column_index = self.cursor_coordinate + self._set_selection_anchor(select) + # TODO: support rows with height > 1 + # rows_to_scroll = 0 + # for ordered_row in self.ordered_rows[: row_index + 1]: + # offset += ordered_row.height + # if offset > height: + # break + # rows_to_scroll += 1 + rows_to_scroll = min(row_index + 1, height) + + self.cursor_coordinate = Coordinate( + row_index - rows_to_scroll + 1, column_index + ) + else: + super().action_page_up() + + def action_scroll_home(self, select: bool = False) -> None: + """Scroll to the top of the data table.""" + self._set_hover_cursor(False) + cursor_type = self.cursor_type + if self.show_cursor and cursor_type in ("cell", "row", "range"): + row_index, column_index = self.cursor_coordinate + self._set_selection_anchor(select) + self.cursor_coordinate = Coordinate(0, column_index) + else: + super().action_scroll_home() + + def action_scroll_end(self, select: bool = False) -> None: + """Scroll to the bottom of the data table.""" + self._set_hover_cursor(False) + cursor_type = self.cursor_type + if self.show_cursor and cursor_type in ("cell", "row", "range"): + row_index, column_index = self.cursor_coordinate + self._set_selection_anchor(select) + self.cursor_coordinate = Coordinate(self.row_count - 1, column_index) + else: + super().action_scroll_end() + + def action_cursor_up(self, select: bool = False) -> None: + self._set_hover_cursor(False) + cursor_type = self.cursor_type + if self.show_cursor and cursor_type in ("cell", "row", "range"): + self._set_selection_anchor(select) + self.cursor_coordinate = self.cursor_coordinate.up() + else: + # If the cursor doesn't move up (e.g. column cursor can't go up), + # then ensure that we instead scroll the DataTable. + super().action_scroll_up() + + def action_cursor_down(self, select: bool = False) -> None: + self._set_hover_cursor(False) + cursor_type = self.cursor_type + if self.show_cursor and cursor_type in ("cell", "row", "range"): + self._set_selection_anchor(select) + self.cursor_coordinate = self.cursor_coordinate.down() + else: + super().action_scroll_down() + + def action_cursor_right(self, select: bool = False) -> None: + self._set_hover_cursor(False) + cursor_type = self.cursor_type + if self.show_cursor and cursor_type in ("cell", "column", "range"): + self._set_selection_anchor(select) + self.cursor_coordinate = self.cursor_coordinate.right() + self._scroll_cursor_into_view(animate=True) + else: + super().action_scroll_right() + + def action_cursor_left(self, select: bool = False) -> None: + self._set_hover_cursor(False) + cursor_type = self.cursor_type + if self.show_cursor and cursor_type in ("cell", "column", "range"): + self._set_selection_anchor(select) + self.cursor_coordinate = self.cursor_coordinate.left() + self._scroll_cursor_into_view(animate=True) + else: + super().action_scroll_left() + + def action_cursor_next(self) -> None: + self._set_hover_cursor(False) + if not self.show_cursor: + super().action_scroll_right() + return + cursor_type = self.cursor_type + self.selection_anchor_coordinate = None + old_coordinate = self.cursor_coordinate + if cursor_type in ("cell", "column", "range"): + self.cursor_coordinate = self.cursor_coordinate.right() + if old_coordinate == self.cursor_coordinate: # at end of row + if old_coordinate.row < self.row_count - 1: + self.cursor_coordinate = Coordinate(old_coordinate.row + 1, 0) + else: + self.cursor_coordinate = Coordinate(0, 0) + elif cursor_type == "row": + self.cursor_coordinate = self.cursor_coordinate.down() + if old_coordinate == self.cursor_coordinate: # at end of table + self.cursor_coordinate = Coordinate(0, 0) + + self._scroll_cursor_into_view(animate=True) + + def action_cursor_prev(self) -> None: + self._set_hover_cursor(False) + if not self.show_cursor: + super().action_scroll_right() + return + cursor_type = self.cursor_type + self.selection_anchor_coordinate = None + old_coordinate = self.cursor_coordinate + if cursor_type in ("cell", "column", "range"): + self.cursor_coordinate = self.cursor_coordinate.left() + if old_coordinate == self.cursor_coordinate: # at start of row + if old_coordinate.row > 0: + self.cursor_coordinate = Coordinate( + old_coordinate.row - 1, self.column_count - 1 + ) + else: + self.cursor_coordinate = Coordinate( + self.row_count - 1, self.column_count - 1 + ) + elif cursor_type == "row": + self.cursor_coordinate = self.cursor_coordinate.up() + if old_coordinate == self.cursor_coordinate: # at start of table + self.cursor_coordinate = Coordinate(self.row_count - 1, 0) + self._scroll_cursor_into_view(animate=True) + + def action_cursor_row_end(self, select: bool = False) -> None: + self._set_hover_cursor(False) + cursor_type = self.cursor_type + if self.show_cursor and cursor_type in ("cell", "column", "range"): + self._set_selection_anchor(select) + self.cursor_coordinate = Coordinate( + self.cursor_coordinate.row, self.column_count - 1 + ) + self._scroll_cursor_into_view(animate=True) + else: + super().action_scroll_right() + + def action_cursor_row_start(self, select: bool = False) -> None: + self._set_hover_cursor(False) + cursor_type = self.cursor_type + if self.show_cursor and cursor_type in ("cell", "column", "range"): + self._set_selection_anchor(select) + self.cursor_coordinate = Coordinate(self.cursor_coordinate.row, 0) + self._scroll_cursor_into_view(animate=True) + else: + super().action_scroll_left() + + def action_cursor_table_end(self, select: bool = False) -> None: + self._set_hover_cursor(False) + self._set_selection_anchor(select) + self.cursor_coordinate = Coordinate(self.row_count - 1, self.column_count - 1) + self._scroll_cursor_into_view(animate=True) + + def action_cursor_table_start(self, select: bool = False) -> None: + self._set_hover_cursor(False) + self._set_selection_anchor(select) + self.cursor_coordinate = Coordinate(0, 0) + self._scroll_cursor_into_view(animate=True) + + def action_select_all(self) -> None: + self._set_hover_cursor(False) + if self.show_cursor and self.cursor_type == "range": + self.selection_anchor_coordinate = Coordinate(0, 0) + self.cursor_coordinate = Coordinate( + self.row_count - 1, self.column_count - 1 + ) + self._scroll_cursor_into_view(animate=True) + + def action_select_cursor(self) -> None: + self._set_hover_cursor(False) + if self.show_cursor and self.cursor_type != "none": + self._post_selected_message() + + def action_copy_selection(self) -> None: + if self.cursor_type == "range" and self.selection_anchor_coordinate is not None: + min_row, max_row, min_col, max_col = self._order_bounding_coords( + self.cursor_coordinate, self.selection_anchor_coordinate + ) + values: list[tuple[Any, ...]] = [] + for row_index in range(min_row, max_row + 1): + values.append( + tuple( + self.get_cell_at(Coordinate(row_index, column_index)) + for column_index in range(min_col, max_col + 1) + ) + ) + elif self.cursor_type in ("cell", "range"): + values = [(self.get_cell_at(self.cursor_coordinate),)] + elif self.cursor_type == "row": + values = [tuple(self.get_row_at(self.cursor_row))] + elif self.cursor_type == "column": + values = [(v,) for v in self.get_column_at(self.cursor_column)] + + self.post_message(DataTable.SelectionCopied(data_table=self, values=values)) + + def _order_bounding_coords(self, *coords: Coordinate) -> tuple[int, int, int, int]: + min_row = min(c.row for c in coords) + max_row = max(c.row for c in coords) + min_col = min(c.column for c in coords) + max_col = max(c.column for c in coords) + return min_row, max_row, min_col, max_col + + def _post_selected_message(self) -> None: + """Post the appropriate message for a selection based on the `cursor_type`.""" + cursor_coordinate = self.cursor_coordinate + cursor_type = self.cursor_type + if self.row_count == 0: + return + if cursor_type == "cell": + self.post_message( + DataTable.CellSelected( + self, + self.get_cell_at(cursor_coordinate), + coordinate=cursor_coordinate, + ) + ) + elif cursor_type == "row": + row_index, _ = cursor_coordinate + self.post_message(DataTable.RowSelected(self, row_index)) + elif cursor_type == "column": + _, column_index = cursor_coordinate + self.post_message(DataTable.ColumnSelected(self, column_index)) diff --git a/src/textual_fastdatatable/formatter.py b/src/textual_fastdatatable/formatter.py new file mode 100644 index 0000000..8088cc5 --- /dev/null +++ b/src/textual_fastdatatable/formatter.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +from datetime import date, datetime, time, timedelta +from decimal import Decimal +from typing import cast + +from rich.align import Align +from rich.console import Console, RenderableType +from rich.errors import MarkupError +from rich.markup import escape +from rich.protocol import is_renderable +from rich.text import Text + +from textual_fastdatatable.column import Column + + +def cell_formatter( + obj: object, null_rep: Text, col: Column | None = None, render_markup: bool = True +) -> RenderableType: + """Convert a cell into a Rich renderable for display. + + For correct formatting, clients should call `locale.setlocale()` first. + + Args: + obj: Data for a cell. + col: Column that the cell came from (used to compute width). + + Returns: + A renderable to be displayed which represents the data. + """ + if obj is None: + return Align(null_rep, align="center") + + elif isinstance(obj, str) and render_markup: + try: + rich_text: Text | str = Text.from_markup(obj) + except MarkupError: + rich_text = escape(obj) + return rich_text + + elif isinstance(obj, str): + return escape(obj) + + elif isinstance(obj, bool): + return Align( + f"[dim]{'✓' if obj else 'X'}[/] {obj}{' ' if obj else ''}", + style="bold" if obj else "", + align="right", + ) + + elif isinstance(obj, (float, Decimal)): + return Align(f"{obj:n}", align="right") + + elif isinstance(obj, int): + if col is not None and col.is_id: + # no separators in ID fields + return Align(str(obj), align="right") + else: + return Align(f"{obj:n}", align="right") + + elif isinstance(obj, (datetime, time)): + + def _fmt_datetime(obj: datetime | time) -> str: + return obj.isoformat(timespec="milliseconds").replace("+00:00", "Z") + + if obj in (datetime.max, datetime.min): + return Align( + ( + f"[bold]{'∞ ' if obj == datetime.max else '-∞ '}[/]" + f"[dim]{_fmt_datetime(obj)}[/]" + ), + align="right", + ) + + return Align(_fmt_datetime(obj), align="right") + + elif isinstance(obj, date): + if obj in (date.max, date.min): + return Align( + ( + f"[bold]{'∞ ' if obj == date.max else '-∞ '}[/]" + f"[dim]{obj.isoformat()}[/]" + ), + align="right", + ) + + return Align(obj.isoformat(), align="right") + + elif isinstance(obj, timedelta): + return Align(str(obj), align="right") + + elif not is_renderable(obj): + return str(obj) + + else: + return cast(RenderableType, obj) + + +def measure_width(obj: object, console: Console) -> int: + renderable = cell_formatter(obj, null_rep=Text("")) + return console.measure(renderable).maximum diff --git a/src/textual_fastdatatable/py.typed b/src/textual_fastdatatable/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/stubs/pyarrow/__init__.pyi b/stubs/pyarrow/__init__.pyi new file mode 100644 index 0000000..0b0b9e3 --- /dev/null +++ b/stubs/pyarrow/__init__.pyi @@ -0,0 +1,166 @@ +from __future__ import annotations + +from typing import Any, Iterable, Iterator, Literal, Mapping, Sequence, Type, TypeVar + +import pandas as pd + +from .compute import CastOptions + +class DataType: ... +class Date32Type(DataType): ... +class Date64Type(DataType): ... +class TimestampType(DataType): ... + +def string() -> DataType: ... +def null() -> DataType: ... +def bool_() -> DataType: ... +def int8() -> DataType: ... +def int16() -> DataType: ... +def int32() -> DataType: ... +def int64() -> DataType: ... +def uint8() -> DataType: ... +def uint16() -> DataType: ... +def uint32() -> DataType: ... +def uint64() -> DataType: ... +def float16() -> DataType: ... +def float32() -> DataType: ... +def float64() -> DataType: ... +def date32() -> DataType: ... +def date64() -> DataType: ... +def binary(length: int = -1) -> DataType: ... +def large_binary() -> DataType: ... +def large_string() -> DataType: ... +def month_day_nano_interval() -> DataType: ... +def time32(unit: Literal["s", "ms", "us", "ns"]) -> DataType: ... +def time64(unit: Literal["s", "ms", "us", "ns"]) -> DataType: ... +def timestamp( + unit: Literal["s", "ms", "us", "ns"], tz: str | None = None +) -> DataType: ... +def duration(unit: Literal["s", "ms", "us", "ns"]) -> DataType: ... + +class MemoryPool: ... +class Schema: ... +class Field: ... +class NativeFile: ... +class MonthDayNano: ... + +class Scalar: + def as_py(self) -> Any: ... + @property + def type(self) -> DataType: ... + +A = TypeVar("A", bound="_PandasConvertible") + +class _PandasConvertible: + @property + def type(self) -> DataType: ... # noqa: A003 + def cast( + self: A, + target_type: DataType | None = None, + safe: bool = True, + options: CastOptions | None = None, + ) -> A: ... + def __getitem__(self, index: int) -> Scalar: ... + def __iter__(self) -> Any: ... + def to_pylist(self) -> list[Any]: ... + def fill_null(self: A, fill_value: Any) -> A: ... + def drop_null(self: A) -> A: ... + +class Array(_PandasConvertible): ... +class ChunkedArray(_PandasConvertible): ... + +class StructArray(Array): + def flatten(self, memory_pool: MemoryPool | None = None) -> list[Array]: ... + +T = TypeVar("T", bound="_Tabular") + +class _Tabular: + @classmethod + def from_arrays( + cls: Type[T], + arrays: list[_PandasConvertible], + names: list[str] | None = None, + schema: Schema | None = None, + metadata: Mapping | None = None, + ) -> T: ... + @classmethod + def from_pydict( + cls: Type[T], + mapping: Mapping, + schema: Schema | None = None, + metadata: Mapping | None = None, + ) -> T: ... + def __getitem__(self, index: int) -> _PandasConvertible: ... + def __len__(self) -> int: ... + @property + def column_names(self) -> list[str]: ... + @property + def columns(self) -> list[_PandasConvertible]: ... + @property + def num_rows(self) -> int: ... + @property + def num_columns(self) -> int: ... + @property + def schema(self) -> Schema: ... + def append_column( + self: T, field_: str | Field, column: Array | ChunkedArray + ) -> T: ... + def column(self, i: int | str) -> _PandasConvertible: ... + def equals(self: T, other: T, check_metadata: bool = False) -> bool: ... + def itercolumns(self) -> Iterator[_PandasConvertible]: ... + def rename_columns(self: T, names: list[str]) -> T: ... + def select(self: T, columns: Sequence[str | int]) -> T: ... + def set_column( + self: T, i: int, field_: str | Field, column: Array | ChunkedArray + ) -> T: ... + def slice( # noqa: A003 + self: T, + offset: int = 0, + length: int | None = None, + ) -> T: ... + def sort_by( + self: T, + sorting: str | list[tuple[str, Literal["ascending", "descending"]]], + **kwargs: Any, + ) -> T: ... + def to_pylist(self) -> list[dict[str, Any]]: ... + +class RecordBatch(_Tabular): ... + +class Table(_Tabular): + @classmethod + def from_batches( + cls, + batches: Iterable[RecordBatch], + schema: Schema | None = None, + ) -> "Table": ... + def to_batches(self) -> list[RecordBatch]: ... + +def scalar(value: Any, type: DataType) -> Scalar: ... # noqa: A002 +def array( + obj: Iterable, + type: DataType | None = None, # noqa: A002 + mask: Array | None = None, + size: int | None = None, + from_pandas: bool | None = None, + safe: bool = True, + memory_pool: MemoryPool | None = None, +) -> Array | ChunkedArray: ... +def concat_arrays( + arrays: Iterable[Array], memory_pool: MemoryPool | None = None +) -> Array: ... +def nulls( + size: int, + type: DataType | None = None, # noqa: A002 + memory_pool: MemoryPool | None = None, +) -> Array: ... +def table( + data: pd.DataFrame + | Mapping[str, _PandasConvertible | list] + | list[_PandasConvertible], + names: list[str] | None = None, + schema: Schema | None = None, + metadata: Mapping | None = None, + nthreads: int | None = None, +) -> Table: ... +def set_timezone_db_path(path: str) -> None: ... diff --git a/stubs/pyarrow/compute.pyi b/stubs/pyarrow/compute.pyi new file mode 100644 index 0000000..a3d7821 --- /dev/null +++ b/stubs/pyarrow/compute.pyi @@ -0,0 +1,64 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Any, Callable, Literal + +from . import DataType, MemoryPool, Scalar, _PandasConvertible + +class Expression: ... +class ScalarAggregateOptions: ... + +class CastOptions: + def __init__( + self, + target_type: DataType | None = None, + allow_int_overflow: bool | None = None, + allow_time_truncate: bool | None = None, + allow_time_overflow: bool | None = None, + allow_decimal_truncate: bool | None = None, + allow_float_truncate: bool | None = None, + allow_invalid_utf8: bool | None = None, + ) -> None: ... + +def max( # noqa: A001 + array: _PandasConvertible, + /, + *, + skip_nulls: bool = True, + min_count: int = 1, + options: ScalarAggregateOptions | None = None, + memory_pool: MemoryPool | None = None, +) -> Scalar: ... +def min( # noqa: A001 + array: _PandasConvertible, + /, + *, + skip_nulls: bool = True, + min_count: int = 1, + options: ScalarAggregateOptions | None = None, + memory_pool: MemoryPool | None = None, +) -> Scalar: ... +def utf8_length( + strings: _PandasConvertible, /, *, memory_pool: MemoryPool | None = None +) -> _PandasConvertible: ... +def register_scalar_function( + func: Callable, + function_name: str, + function_doc: dict[Literal["summary", "description"], str], + in_types: dict[str, DataType], + out_type: DataType, + func_registry: Any | None = None, +) -> None: ... +def call_function( + function_name: str, target: list[_PandasConvertible] +) -> _PandasConvertible: ... +def assume_timezone( + timestamps: _PandasConvertible | Scalar | datetime, + /, + timezone: str, + *, + ambiguous: Literal["raise", "earliest", "latest"] = "raise", + nonexistent: Literal["raise", "earliest", "latest"] = "raise", + options: Any | None = None, + memory_pool: MemoryPool | None = None, +) -> _PandasConvertible: ... diff --git a/stubs/pyarrow/dataset.pyi b/stubs/pyarrow/dataset.pyi new file mode 100644 index 0000000..24263e3 --- /dev/null +++ b/stubs/pyarrow/dataset.pyi @@ -0,0 +1 @@ +class Partitioning: ... diff --git a/stubs/pyarrow/fs.pyi b/stubs/pyarrow/fs.pyi new file mode 100644 index 0000000..df83021 --- /dev/null +++ b/stubs/pyarrow/fs.pyi @@ -0,0 +1 @@ +class FileSystem: ... diff --git a/stubs/pyarrow/lib.pyi b/stubs/pyarrow/lib.pyi new file mode 100644 index 0000000..46d26bc --- /dev/null +++ b/stubs/pyarrow/lib.pyi @@ -0,0 +1,32 @@ +from . import Date32Type, Date64Type, Scalar, TimestampType + +class ArrowException(Exception): ... +class ArrowInvalid(ValueError, ArrowException): ... +class ArrowMemoryError(MemoryError, ArrowException): ... +class ArrowKeyError(KeyError, Exception): ... +class ArrowTypeError(TypeError, Exception): ... +class ArrowNotImplementedError(NotImplementedError, ArrowException): ... +class ArrowCapacityError(ArrowException): ... +class ArrowIndexError(IndexError, ArrowException): ... +class ArrowSerializationError(ArrowException): ... +class ArrowCancelled(ArrowException): ... + +ArrowIOError = IOError + +class Date32Scalar(Scalar): + @property + def type(self) -> Date32Type: ... + @property + def value(self) -> int: ... + +class Date64Scalar(Scalar): + @property + def type(self) -> Date64Type: ... + @property + def value(self) -> int: ... + +class TimestampScalar(Scalar): + @property + def type(self) -> TimestampType: ... + @property + def value(self) -> int: ... diff --git a/stubs/pyarrow/parquet.pyi b/stubs/pyarrow/parquet.pyi new file mode 100644 index 0000000..7c2ef52 --- /dev/null +++ b/stubs/pyarrow/parquet.pyi @@ -0,0 +1,60 @@ +from __future__ import annotations + +from typing import Any, BinaryIO, Literal + +from . import NativeFile, Schema, Table +from .compute import Expression +from .dataset import Partitioning +from .fs import FileSystem + +class FileMetaData: ... + +def read_table( + source: str | NativeFile | BinaryIO, + *, + columns: list | None = None, + use_threads: bool = True, + metadata: FileMetaData | None = None, + schema: Schema | None = None, + use_pandas_metadata: bool = False, + read_dictionary: list | None = None, + memory_map: bool = False, + buffer_size: int = 0, + partitioning: Partitioning | str | list[str] = "hive", + filesystem: FileSystem | None = None, + filters: Expression | list[tuple] | list[list[tuple]] | None = None, + use_legacy_dataset: bool = False, + ignore_prefixes: list | None = None, + pre_buffer: bool = True, + coerce_int96_timestamp_unit: str | None = None, + decryption_properties: Any | None = None, + thrift_string_size_limit: int | None = None, + thrift_container_size_limit: int | None = None, +) -> Table: ... +def write_table( + table: Table, + where: str | NativeFile, + row_group_size: int | None = None, + version: Literal["1.0", "2.4", "2.6"] = "2.6", + use_dictionary: bool | list = True, + compression: Literal["none", "snappy", "gzip", "brotli", "lz4", "zstd"] + | dict[str, Literal["none", "snappy", "gzip", "brotli", "lz4", "zstd"]] = "snappy", + write_statistics: bool | list = True, + use_deprecated_int96_timestamps: bool | None = None, + coerce_timestamps: str | None = None, + allow_truncated_timestamps: bool = False, + data_page_size: int | None = None, + flavor: Literal["spark"] | None = None, + filesystem: FileSystem | None = None, + compression_level: int | dict | None = None, + use_byte_stream_split: bool | list = False, + column_encoding: str | dict | None = None, + data_page_version: Literal["1.0", "2.0"] = "1.0", + use_compliant_nested_type: bool = True, + encryption_properties: Any | None = None, + write_batch_size: int | None = None, + dictionary_pagesize_limit: int | None = None, + store_schema: bool = True, + write_page_index: bool = False, + **kwargs: Any, +) -> None: ... diff --git a/stubs/pyarrow/types.pyi b/stubs/pyarrow/types.pyi new file mode 100644 index 0000000..b71b3c5 --- /dev/null +++ b/stubs/pyarrow/types.pyi @@ -0,0 +1,27 @@ +from __future__ import annotations + +from . import DataType, Date32Type, Date64Type, TimestampType + +def is_null(t: DataType) -> bool: ... +def is_struct(t: DataType) -> bool: ... +def is_boolean(t: DataType) -> bool: ... +def is_integer(t: DataType) -> bool: ... +def is_floating(t: DataType) -> bool: ... +def is_decimal(t: DataType) -> bool: ... +def is_temporal(t: DataType) -> bool: ... +def is_date(t: DataType) -> bool: ... +def is_date32(t: DataType) -> bool: + if isinstance(t, Date32Type): + return True + return False + +def is_date64(t: DataType) -> bool: + if isinstance(t, Date64Type): + return True + return False + +def is_time(t: DataType) -> bool: ... +def is_timestamp(t: DataType) -> bool: + if isinstance(t, TimestampType): + return True + return False diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..493bc70 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from typing import Sequence, Type + +import pytest +from textual_fastdatatable.backend import ArrowBackend, DataTableBackend, PolarsBackend + + +@pytest.fixture +def pydict() -> dict[str, Sequence[str | int]]: + return { + "first column": [1, 2, 3, 4, 5], + "two": ["a", "b", "c", "d", "asdfasdf"], + "three": ["foo", "bar", "baz", "qux", "foofoo"], + } + + +@pytest.fixture +def records(pydict: dict[str, Sequence[str | int]]) -> list[tuple[str | int, ...]]: + header = tuple(pydict.keys()) + cols = list(pydict.values()) + num_rows = len(cols[0]) + data = [tuple([col[i] for col in cols]) for i in range(num_rows)] + return [header, *data] + + +@pytest.fixture(params=[ArrowBackend, PolarsBackend]) +def backend( + request: Type[pytest.FixtureRequest], pydict: dict[str, Sequence[str | int]] +) -> DataTableBackend: + backend_cls = request.param + assert issubclass(backend_cls, (ArrowBackend, PolarsBackend)) + backend: ArrowBackend | PolarsBackend = backend_cls.from_pydict(pydict) + return backend diff --git a/tests/data/lap_times_100.parquet b/tests/data/lap_times_100.parquet new file mode 100644 index 0000000..f26b428 Binary files /dev/null and b/tests/data/lap_times_100.parquet differ diff --git a/tests/data/lap_times_1000.parquet b/tests/data/lap_times_1000.parquet new file mode 100644 index 0000000..5878699 Binary files /dev/null and b/tests/data/lap_times_1000.parquet differ diff --git a/tests/data/lap_times_10000.parquet b/tests/data/lap_times_10000.parquet new file mode 100644 index 0000000..91f6bf2 Binary files /dev/null and b/tests/data/lap_times_10000.parquet differ diff --git a/tests/data/lap_times_100000.parquet b/tests/data/lap_times_100000.parquet new file mode 100644 index 0000000..682fc65 Binary files /dev/null and b/tests/data/lap_times_100000.parquet differ diff --git a/tests/data/lap_times_538121.parquet b/tests/data/lap_times_538121.parquet new file mode 100644 index 0000000..c6ef432 Binary files /dev/null and b/tests/data/lap_times_538121.parquet differ diff --git a/tests/data/wide_10000.parquet b/tests/data/wide_10000.parquet new file mode 100644 index 0000000..2f8ac6f Binary files /dev/null and b/tests/data/wide_10000.parquet differ diff --git a/tests/data/wide_100000.parquet b/tests/data/wide_100000.parquet new file mode 100644 index 0000000..347fa00 Binary files /dev/null and b/tests/data/wide_100000.parquet differ diff --git a/tests/snapshot_tests/LICENSE b/tests/snapshot_tests/LICENSE new file mode 100644 index 0000000..3a43997 --- /dev/null +++ b/tests/snapshot_tests/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Will McGugan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr new file mode 100644 index 0000000..d1c221c --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -0,0 +1,2924 @@ +# serializer version: 1 +# name: test_auto_table + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MyApp + + + + + + + + + + ⭘                                                     MyApp                                                  + ╭──────────────────╮╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + ok                ││test                                                                                               + ╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍││╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ + ││╭─ 0 ────────────────────────────────────────╮╭─ 1 ────────────────────────────────────────╮╭─ 2 ─│ + │││││││ + │││ Foo       Bar         Baz                ││ Foo       Bar         Baz                ││ Foo  + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ▁▁││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ▁▁││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + │││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXYZ ││ ABCD + ││╰────────────────────────────────────────────╯╰────────────────────────────────────────────╯╰─────│ + ││ + ╰──────────────────╯╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + + ''' +# --- +# name: test_datatable_add_column + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AddColumn + + + + + + + + + +  Movies          No Default  With Default  Long Default          +  Severance       ABC           01234567890123456789  +  Foundation      ABC           01234567890123456789  +  Dark            Hello!      ABC           01234567890123456789  +  The Boys        ABC           01234567890123456789  +  The Last of Us  ABC           01234567890123456789  +  Lost in Space   ABC           01234567890123456789  +  Altered Carbon  ABC           01234567890123456789  + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_column_cursor_render + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + +  lane  swimmer               country        time   +  4     Joseph Schooling      Singapore      50.39  +  2     Michael Phelps        United States  51.14  +  5     Chad le Clos          South Africa   51.14  +  6     László Cseh           Hungary        51.14  +  3     Li Zhuhao             China          51.26  +  8     Mehdy Metella         France         51.58  +  7     Tom Shields           United States  51.73  +  1     Aleksandr Sadovnikov  Russia         51.84  + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_empty + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_empty_add_col + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + +  Foo  +  1    +  2    + + + + + + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_from_parquet + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + +  raceId  driverId  lap  position  time      milliseconds  +     841        20    1         1  1:38.109         98109  +     841        20    2         1  1:33.006         93006  +     841        20    3         1  1:32.713         92713  +     841        20    4         1  1:32.803         92803  +     841        20    5         1  1:32.342         92342 ▂▂ +     841        20    6         1  1:32.605         92605  +     841        20    7         1  1:32.502         92502  +     841        20    8         1  1:32.537         92537  +     841        20    9         1  1:33.240         93240  +     841        20   10         1  1:32.572         92572  +     841        20   11         1  1:32.669         92669  +     841        20   12         1  1:32.902         92902  +     841        20   13         1  1:33.698         93698  +     841        20   14         3  1:52.075        112075  +     841        20   15         4  1:38.385         98385  +     841        20   16         2  1:31.548         91548  +     841        20   17         1  1:30.800         90800  +     841        20   18         1  1:31.810         91810  +     841        20   19         1  1:31.018         91018  +     841        20   20         1  1:31.055         91055  +     841        20   21         1  1:31.288         91288  +     841        20   22         1  1:31.084         91084  +     841        20   23         1  1:30.875         90875  + + + + + ''' +# --- +# name: test_datatable_from_pydict + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + + Not Foo Zig        Zag +        0  0123456789  IJKLMNOPQRSTUVWXYZ  +        1  0123456789  IJKLMNOPQRSTUVWXYZ  +        2  0123456789  IJKLMNOPQRSTUVWXYZ  +        3  0123456789  IJKLMNOPQRSTUVWXYZ  +        4  0123456789  IJKLMNOPQRSTUVWXYZ  +        5  0123456789  IJKLMNOPQRSTUVWXYZ  +        6  0123456789  IJKLMNOPQRSTUVWXYZ  +        7  0123456789  IJKLMNOPQRSTUVWXYZ  +        8  0123456789  IJKLMNOPQRSTUVWXYZ  +        9  0123456789  IJKLMNOPQRSTUVWXYZ  +       10  0123456789  IJKLMNOPQRSTUVWXYZ ▅▅ +       11  0123456789  IJKLMNOPQRSTUVWXYZ  +       12  0123456789  IJKLMNOPQRSTUVWXYZ  +       13  0123456789  IJKLMNOPQRSTUVWXYZ  +       14  0123456789  IJKLMNOPQRSTUVWXYZ  +       15  0123456789  IJKLMNOPQRSTUVWXYZ  +       16  0123456789  IJKLMNOPQRSTUVWXYZ  +       17  0123456789  IJKLMNOPQRSTUVWXYZ  +       18  0123456789  IJKLMNOPQRSTUVWXYZ  +       19  0123456789  IJKLMNOPQRSTUVWXYZ  +       20  0123456789  IJKLMNOPQRSTUVWXYZ  +       21  0123456789  IJKLMNOPQRSTUVWXYZ  +       22  0123456789  IJKLMNOPQRSTUVWXYZ  + + + + + ''' +# --- +# name: test_datatable_from_records + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + +  lane  swimmer               country        time   +     4  Joseph Schooling      Singapore      50.39  +     2  Michael Phelps        United States  51.14  +     5  Chad le Clos          South Africa   51.14  +     6  László Cseh           Hungary        51.14  +     3  Li Zhuhao             China          51.26  +     8  Mehdy Metella         France         51.58  +     7  Tom Shields           United States  51.73  +     1  Aleksandr Sadovnikov  Russia         51.84  +    10  Darren Burns          Scotland       51.84  + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_max_width_render + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + +  lane  swimmer   country   time   +  4     Joseph …  Singapo…  50.39  +  2     Michael…  United …  51.14  +  5     Chad le…  South A…  51.14  +  6     László …  Hungary   51.14  +  3     Li Zhuh…  China     51.26  +  8     Mehdy M…  France    51.58  +  7     Tom Shi…  United …  51.73  +  1     Aleksan…  Russia    51.84  + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_no_render_markup + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + +  lane  swimmer                 country        time   +     4  [Joseph Schooling]      Singapore      50.39  +     2  [red]Michael Phelps[/]  United States  51.14  +     5  [bold]Chad le Clos[/]   South Africa   51.14  +     6  László Cseh             Hungary        51.14  +     3  Li Zhuhao               China          51.26  +     8  Mehdy Metella           France         51.58  +     7  Tom Shields             United States  51.73  +     1  Aleksandr Sadovnikov    Russia         51.84  +    10  Darren Burns            Scotland       51.84  + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_no_rows + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + +  foo foo bar  + + + + + + + + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_no_rows_empty_sequence + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + +  foo foo bar  + + + + + + + + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_null_mixed_cols + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + +  lane   swimmer               country        time   +  3      Li Zhuhao             China          51.26  +  eight ∅ null France         51.58  +  seven  Tom Shields           United States ∅  +  1      Aleksandr Sadovnikov  Russia         51.84  + ∅  Darren Burns          Scotland       51.84  + + + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_range_cursor_render + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + +  lane  swimmer               country        time   +  4     Joseph Schooling      Singapore      50.39  +  2     Michael Phelps        United States  51.14  +  5     Chad le Clos          South Africa   51.14  +  6     László Cseh           Hungary        51.14  +  3     Li Zhuhao             China          51.26  +  8     Mehdy Metella         France         51.58  +  7     Tom Shields           United States  51.73  +  1     Aleksandr Sadovnikov  Russia         51.84  + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_remove_row + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + +  lane  swimmer               country        time   +     5  Chad le Clos          South Africa   51.14  +     4  Joseph Schooling      Singapore      50.39  +     6  László Cseh           Hungary        51.14  +     3  Li Zhuhao             China          51.26  +     7  Tom Shields           United States  51.73  +    10  Darren Burns          Scotland       51.84  + + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_render + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + +  lane  swimmer               country        time   +     4  Joseph Schooling      Singapore      50.39  +     2  Michael Phelps        United States  51.14  +     5  Chad le Clos          South Africa   51.14  +     6  László Cseh           Hungary        51.14  +     3  Li Zhuhao             China          51.26  +     8  Mehdy Metella         France         51.58  +     7  Tom Shields           United States  51.73  +     1  Aleksandr Sadovnikov  Russia         51.84  +    10  Darren Burns          Scotland       51.84  + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_row_cursor_render + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + +  lane  swimmer               country        time   +  4     Joseph Schooling      Singapore      50.39  +  2     Michael Phelps        United States  51.14  +  5     Chad le Clos          South Africa   51.14  +  6     László Cseh           Hungary        51.14  +  3     Li Zhuhao             China          51.26  +  8     Mehdy Metella         France         51.58  +  7     Tom Shields           United States  51.73  +  1     Aleksandr Sadovnikov  Russia         51.84  + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_sort_multikey + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + +  lane  swimmer               country        time   +     4  Joseph Schooling      Singapore      50.39  +     2  Michael Phelps        United States  51.14  +     5  Chad le Clos          South Africa   51.14  +     6  László Cseh           Hungary        51.14  +     3  Li Zhuhao             China          51.26  +     8  Mehdy Metella         France         51.58  +     7  Tom Shields           United States  51.73  +     1  Aleksandr Sadovnikov  Russia         51.84  +    10  Darren Burns          Scotland       51.84  + + + + + + + + + + + + + + + + + + + ''' +# --- diff --git a/tests/snapshot_tests/_snapshots_backup/test_snapshots.ambr b/tests/snapshot_tests/_snapshots_backup/test_snapshots.ambr new file mode 100644 index 0000000..6840bac --- /dev/null +++ b/tests/snapshot_tests/_snapshots_backup/test_snapshots.ambr @@ -0,0 +1,1814 @@ +# name: test_auto_table + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MyApp + + + + + + + + + + MyApp + ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + oktest + ╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ +  0 ────────────────────────────────────── 1 ────────────────────────────────────── 2 ───── + +  Foo       Bar         Baz               Foo       Bar         Baz               Foo      +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY▁▁ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY▁▁ ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH + ───────────────────────────────────────────────────────────────────────────────────────────── + + ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + + + ''' +# --- +# name: test_datatable_add_column + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AddColumn + + + + + + + + + +  Movies          No Default  With Default  Long Default          +  Severance       ABC           01234567890123456789  +  Foundation      ABC           01234567890123456789  +  Dark            Hello!      ABC           01234567890123456789  +  The Boys        ABC           01234567890123456789  +  The Last of Us  ABC           01234567890123456789  +  Lost in Space   ABC           01234567890123456789  +  Altered Carbon  ABC           01234567890123456789  + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_add_row_auto_height + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AutoHeightRowsApp + + + + + + + + + +  N  Column      +  3  hey there   +  1  hey there   +  5  long        +  string      +  2  ╭───────╮   +  │ Hello │   +  │ world │   +  ╰───────╯   +  4  1           +  2           +  3           +  4           +  5           +  6           +  7           + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_add_row_auto_height_sorted + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AutoHeightRowsApp + + + + + + + + + +  N  Column      +  1  hey there   +  2  ╭───────╮   +  │ Hello │   +  │ world │   +  ╰───────╯   +  3  hey there   +  4  1           +  2           +  3           +  4           +  5           +  6           +  7           +  5  long        +  string      + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_column_cursor_render + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + +  lane  swimmer               country        time   +  4     Joseph Schooling      Singapore      50.39  +  2     Michael Phelps        United States  51.14  +  5     Chad le Clos          South Africa   51.14  +  6     László Cseh           Hungary        51.14  +  3     Li Zhuhao             China          51.26  +  8     Mehdy Metella         France         51.58  +  7     Tom Shields           United States  51.73  +  1     Aleksandr Sadovnikov  Russia         51.84  + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_labels_and_fixed_data + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + +  lane  swimmer               country        time   +  0  5     Chad le Clos          South Africa   51.14  +  1  4     Joseph Schooling      Singapore      50.39  +  2  2     Michael Phelps        United States  51.14  +  3  6     László Cseh           Hungary        51.14  +  4  3     Li Zhuhao             China          51.26  +  5  8     Mehdy Metella         France         51.58  +  6  7     Tom Shields           United States  51.73  +  7  10    Darren Burns          Scotland       51.84  +  8  1     Aleksandr Sadovnikov  Russia         51.84  + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_remove_row + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + +  lane  swimmer               country        time   +  5     Chad le Clos          South Africa   51.14  +  4     Joseph Schooling      Singapore      50.39  +  6     László Cseh           Hungary        51.14  +  3     Li Zhuhao             China          51.26  +  7     Tom Shields           United States  51.73  +  10    Darren Burns          Scotland       51.84  + + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_render + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + +  lane  swimmer               country        time   +  4     Joseph Schooling      Singapore      50.39  +  2     Michael Phelps        United States  51.14  +  5     Chad le Clos          South Africa   51.14  +  6     László Cseh           Hungary        51.14  +  3     Li Zhuhao             China          51.26  +  8     Mehdy Metella         France         51.58  +  7     Tom Shields           United States  51.73  +  1     Aleksandr Sadovnikov  Russia         51.84  +  10    Darren Burns          Scotland       51.84  + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_row_cursor_render + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + +  lane  swimmer               country        time   +  4     Joseph Schooling      Singapore      50.39  +  2     Michael Phelps        United States  51.14  +  5     Chad le Clos          South Africa   51.14  +  6     László Cseh           Hungary        51.14  +  3     Li Zhuhao             China          51.26  +  8     Mehdy Metella         France         51.58  +  7     Tom Shields           United States  51.73  +  1     Aleksandr Sadovnikov  Russia         51.84  + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_sort_multikey + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + +  lane  swimmer               country        time   +  4     Joseph Schooling      Singapore      50.39  +  2     Michael Phelps        United States  51.14  +  5     Chad le Clos          South Africa   51.14  +  6     László Cseh           Hungary        51.14  +  3     Li Zhuhao             China          51.26  +  8     Mehdy Metella         France         51.58  +  7     Tom Shields           United States  51.73  +  1     Aleksandr Sadovnikov  Russia         51.84  +  10    Darren Burns          Scotland       51.84  + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_style_ordering + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DataTableCursorStyles + + + + + + + + + + Foreground is 'css', background is 'css': +  Movies      +  Severance   + Foundation + Dark + + Foreground is 'css', background is 'renderable': +  Movies      + Severance + Foundation + Dark + + Foreground is 'renderable', background is 'renderable': +  Movies      + Severance + Foundation + Dark + + Foreground is 'renderable', background is 'css': +  Movies      + Severance + Foundation + Dark + + + + + + ''' +# --- diff --git a/tests/snapshot_tests/snapshot_apps/auto-table.py b/tests/snapshot_tests/snapshot_apps/auto-table.py new file mode 100644 index 0000000..9aea8d8 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/auto-table.py @@ -0,0 +1,185 @@ +from textual.app import App +from textual.containers import Container, Horizontal, ScrollableContainer, Vertical +from textual.screen import Screen +from textual.widgets import Header, Label +from textual_fastdatatable import ArrowBackend, DataTable + + +class LabeledBox(Container): + DEFAULT_CSS = """ + LabeledBox { + layers: base_ top_; + width: 100%; + height: 100%; + } + + LabeledBox > Container { + layer: base_; + border: round $primary; + width: 100%; + height: 100%; + layout: vertical; + } + + LabeledBox > Label { + layer: top_; + offset-x: 2; + } + """ + + def __init__(self, title, *args, **kwargs): + self.__label = Label(title) + + super().__init__(self.__label, Container(*args, **kwargs)) + + @property + def label(self): + return self.__label + + +class StatusTable(DataTable): + def __init__(self) -> None: + backend = ArrowBackend.from_pydict( + { + "Foo": ["ABCDEFGH"] * 50, + "Bar": ["0123456789"] * 50, + "Baz": ["IJKLMNOPQRSTUVWXYZ"] * 50, + } + ) + super().__init__(backend=backend) + + self.cursor_type = "row" + self.show_cursor = False + + +class Status(LabeledBox): + DEFAULT_CSS = """ + Status { + width: auto; + } + + Status Container { + width: auto; + } + + Status StatusTable { + width: auto; + height: 100%; + margin-top: 1; + scrollbar-gutter: stable; + overflow-x: hidden; + } + """ + + def __init__(self, name: str): + self.__name = name + self.__table = StatusTable() + + super().__init__(f" {self.__name} ", self.__table) + + @property + def name(self) -> str: + return self.__name + + @property + def table(self) -> StatusTable: + return self.__table + + +class Rendering(LabeledBox): + DEFAULT_CSS = """ + #issue-info { + height: auto; + border-bottom: dashed #632CA6; + } + + #statuses-box { + height: 1fr; + width: auto; + } + """ + + def __init__(self): + self.__info = Label("test") + + super().__init__( + "", + ScrollableContainer( + Horizontal(self.__info, id="issue-info"), + Horizontal(*[Status(str(i)) for i in range(4)], id="statuses-box"), + id="issues-box", + ), + ) + + @property + def info(self) -> Label: + return self.__info + + +class Sidebar(LabeledBox): + DEFAULT_CSS = """ + #sidebar-status { + height: auto; + border-bottom: dashed #632CA6; + } + + #sidebar-options { + height: 1fr; + } + """ + + def __init__(self): + self.__status = Label("ok") + self.__options = Vertical() + + super().__init__( + "", + Container(self.__status, id="sidebar-status"), + Container(self.__options, id="sidebar-options"), + ) + + @property + def status(self) -> Label: + return self.__status + + @property + def options(self) -> Vertical: + return self.__options + + +class MyScreen(Screen): + DEFAULT_CSS = """ + #main-content { + layout: grid; + grid-size: 2; + grid-columns: 1fr 5fr; + grid-rows: 1fr; + } + + #main-content-sidebar { + height: 100%; + } + + #main-content-rendering { + height: 100%; + } + """ + + def compose(self): + yield Header() + yield Container( + Container(Sidebar(), id="main-content-sidebar"), + Container(Rendering(), id="main-content-rendering"), + id="main-content", + ) + + +class MyApp(App): + async def on_mount(self): + self.install_screen(MyScreen(), "myscreen") + await self.push_screen("myscreen") + + +if __name__ == "__main__": + app = MyApp() + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/data_table.py b/tests/snapshot_tests/snapshot_apps/data_table.py new file mode 100644 index 0000000..5ab2f16 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/data_table.py @@ -0,0 +1,26 @@ +from textual.app import App, ComposeResult +from textual_fastdatatable import ArrowBackend, DataTable + +ROWS = [ + ("lane", "swimmer", "country", "time"), + (4, "Joseph Schooling", "Singapore", 50.39), + (2, "Michael Phelps", "United States", 51.14), + (5, "Chad le Clos", "South Africa", 51.14), + (6, "László Cseh", "Hungary", 51.14), + (3, "Li Zhuhao", "China", 51.26), + (8, "Mehdy Metella", "France", 51.58), + (7, "Tom Shields", "United States", 51.73), + (1, "Aleksandr Sadovnikov", "Russia", 51.84), + (10, "Darren Burns", "Scotland", 51.84), +] + + +class TableApp(App): + def compose(self) -> ComposeResult: + backend = ArrowBackend.from_records(ROWS, has_header=True) + yield DataTable(backend=backend) + + +app = TableApp() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/data_table_add_column.py b/tests/snapshot_tests/snapshot_apps/data_table_add_column.py new file mode 100644 index 0000000..083e0f5 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/data_table_add_column.py @@ -0,0 +1,36 @@ +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual_fastdatatable import ArrowBackend, DataTable + +MOVIES = [ + "Severance", + "Foundation", + "Dark", + "The Boys", + "The Last of Us", + "Lost in Space", + "Altered Carbon", +] + + +class AddColumn(App): + BINDINGS = [ + Binding(key="c", action="add_column", description="Add Column"), + ] + + def compose(self) -> ComposeResult: + backend = ArrowBackend.from_pydict({"Movies": MOVIES}) + table = DataTable(backend=backend) + + column_idx = table.add_column("No Default") + table.add_column("With Default", default="ABC") + table.add_column("Long Default", default="01234567890123456789") + + # Ensure we can update a cell + table.update_cell(2, column_idx, "Hello!") + yield table + + +app = AddColumn() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/data_table_add_row_auto_height.py b/tests/snapshot_tests/snapshot_apps/data_table_add_row_auto_height.py new file mode 100644 index 0000000..482aff3 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/data_table_add_row_auto_height.py @@ -0,0 +1,24 @@ +from rich.panel import Panel +from rich.text import Text +from textual.app import App +from textual_fastdatatable import DataTable + + +class AutoHeightRowsApp(App[None]): + def compose(self): + table = DataTable() + self.column = table.add_column("N") + table.add_column("Column", width=10) + table.add_row(3, "hey there", height=None) + table.add_row(1, Text("hey there"), height=None) + table.add_row(5, Text("long string", overflow="fold"), height=None) + table.add_row(2, Panel.fit("Hello\nworld"), height=None) + table.add_row(4, "1\n2\n3\n4\n5\n6\n7", height=None) + yield table + + def key_s(self): + self.query_one(DataTable).sort(self.column) + + +if __name__ == "__main__": + AutoHeightRowsApp().run() diff --git a/tests/snapshot_tests/snapshot_apps/data_table_column_cursor.py b/tests/snapshot_tests/snapshot_apps/data_table_column_cursor.py new file mode 100644 index 0000000..b657c25 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/data_table_column_cursor.py @@ -0,0 +1,35 @@ +import csv +import io + +from textual.app import App, ComposeResult +from textual_fastdatatable import ArrowBackend, DataTable + +CSV = """lane,swimmer,country,time +4,Joseph Schooling,Singapore,50.39 +2,Michael Phelps,United States,51.14 +5,Chad le Clos,South Africa,51.14 +6,László Cseh,Hungary,51.14 +3,Li Zhuhao,China,51.26 +8,Mehdy Metella,France,51.58 +7,Tom Shields,United States,51.73 +1,Aleksandr Sadovnikov,Russia,51.84""" + + +class TableApp(App): + def compose(self) -> ComposeResult: + rows = csv.reader(io.StringIO(CSV)) + labels = next(rows) + data = [row for row in rows] + backend = ArrowBackend.from_pydict( + {label: [row[i] for row in data] for i, label in enumerate(labels)} + ) + table = DataTable( + backend=backend, cursor_type="column", fixed_columns=1, fixed_rows=1 + ) + table.focus() + yield table + + +if __name__ == "__main__": + app = TableApp() + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/data_table_max_width.py b/tests/snapshot_tests/snapshot_apps/data_table_max_width.py new file mode 100644 index 0000000..443e7f6 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/data_table_max_width.py @@ -0,0 +1,35 @@ +import csv +import io + +from textual.app import App, ComposeResult +from textual_fastdatatable import ArrowBackend, DataTable + +CSV = """lane,swimmer,country,time +4,Joseph Schooling,Singapore,50.39 +2,Michael Phelps,United States,51.14 +5,Chad le Clos,South Africa,51.14 +6,László Cseh,Hungary,51.14 +3,Li Zhuhao,China,51.26 +8,Mehdy Metella,France,51.58 +7,Tom Shields,United States,51.73 +1,Aleksandr Sadovnikov,Russia,51.84""" + + +class TableApp(App): + def compose(self) -> ComposeResult: + rows = csv.reader(io.StringIO(CSV)) + labels = next(rows) + data = [row for row in rows] + backend = ArrowBackend.from_pydict( + {label: [row[i] for row in data] for i, label in enumerate(labels)} + ) + table = DataTable( + backend=backend, cursor_type="range", max_column_content_width=8 + ) + table.focus() + yield table + + +if __name__ == "__main__": + app = TableApp() + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/data_table_no_render_markup.py b/tests/snapshot_tests/snapshot_apps/data_table_no_render_markup.py new file mode 100644 index 0000000..968e954 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/data_table_no_render_markup.py @@ -0,0 +1,26 @@ +from textual.app import App, ComposeResult +from textual_fastdatatable import ArrowBackend, DataTable + +ROWS = [ + ("lane", "swimmer", "country", "time"), + (4, "[Joseph Schooling]", "Singapore", 50.39), + (2, "[red]Michael Phelps[/]", "United States", 51.14), + (5, "[bold]Chad le Clos[/]", "South Africa", 51.14), + (6, "László Cseh", "Hungary", 51.14), + (3, "Li Zhuhao", "China", 51.26), + (8, "Mehdy Metella", "France", 51.58), + (7, "Tom Shields", "United States", 51.73), + (1, "Aleksandr Sadovnikov", "Russia", 51.84), + (10, "Darren Burns", "Scotland", 51.84), +] + + +class TableApp(App): + def compose(self) -> ComposeResult: + backend = ArrowBackend.from_records(ROWS, has_header=True) + yield DataTable(backend=backend, render_markup=False) + + +app = TableApp() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/data_table_null_mixed_cols.py b/tests/snapshot_tests/snapshot_apps/data_table_null_mixed_cols.py new file mode 100644 index 0000000..9158364 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/data_table_null_mixed_cols.py @@ -0,0 +1,22 @@ +from textual.app import App, ComposeResult +from textual_fastdatatable import ArrowBackend, DataTable + +ROWS = [ + ("lane", "swimmer", "country", "time"), + (3, "Li Zhuhao", "China", 51.26), + ("eight", None, "France", 51.58), + ("seven", "Tom Shields", "United States", None), + (1, "Aleksandr Sadovnikov", "Russia", 51.84), + (None, "Darren Burns", "Scotland", 51.84), +] + + +class TableApp(App): + def compose(self) -> ComposeResult: + backend = ArrowBackend.from_records(ROWS, has_header=True) + yield DataTable(backend=backend, null_rep="[dim]∅ null[/]") + + +app = TableApp() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/data_table_range_cursor.py b/tests/snapshot_tests/snapshot_apps/data_table_range_cursor.py new file mode 100644 index 0000000..836d6a8 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/data_table_range_cursor.py @@ -0,0 +1,33 @@ +import csv +import io + +from textual.app import App, ComposeResult +from textual_fastdatatable import ArrowBackend, DataTable + +CSV = """lane,swimmer,country,time +4,Joseph Schooling,Singapore,50.39 +2,Michael Phelps,United States,51.14 +5,Chad le Clos,South Africa,51.14 +6,László Cseh,Hungary,51.14 +3,Li Zhuhao,China,51.26 +8,Mehdy Metella,France,51.58 +7,Tom Shields,United States,51.73 +1,Aleksandr Sadovnikov,Russia,51.84""" + + +class TableApp(App): + def compose(self) -> ComposeResult: + rows = csv.reader(io.StringIO(CSV)) + labels = next(rows) + data = [row for row in rows] + backend = ArrowBackend.from_pydict( + {label: [row[i] for row in data] for i, label in enumerate(labels)} + ) + table = DataTable(backend=backend, cursor_type="range") + table.focus() + yield table + + +if __name__ == "__main__": + app = TableApp() + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/data_table_remove_row.py b/tests/snapshot_tests/snapshot_apps/data_table_remove_row.py new file mode 100644 index 0000000..154c635 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/data_table_remove_row.py @@ -0,0 +1,45 @@ +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual_fastdatatable import ArrowBackend, DataTable + +ROWS = [ + ("lane", "swimmer", "country", "time"), + (5, "Chad le Clos", "South Africa", 51.14), + (4, "Joseph Schooling", "Singapore", 50.39), + (2, "Michael Phelps", "United States", 51.14), + (6, "László Cseh", "Hungary", 51.14), + (3, "Li Zhuhao", "China", 51.26), + (8, "Mehdy Metella", "France", 51.58), + (7, "Tom Shields", "United States", 51.73), + (10, "Darren Burns", "Scotland", 51.84), + (1, "Aleksandr Sadovnikov", "Russia", 51.84), +] + + +class TableApp(App): + """Snapshot app for testing removal of rows. + Removes several rows, so we can check that the display of the + DataTable updates as expected.""" + + BINDINGS = [ + Binding("r", "remove_row", "Remove Row"), + ] + + def compose(self) -> ComposeResult: + backend = ArrowBackend.from_records(ROWS, has_header=True) + yield DataTable(backend=backend) + + def on_mount(self) -> None: + table = self.query_one(DataTable) + table.focus() + + def action_remove_row(self): + table = self.query_one(DataTable) + table.remove_row(2) + table.remove_row(4) + table.remove_row(6) + + +app = TableApp() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/data_table_row_cursor.py b/tests/snapshot_tests/snapshot_apps/data_table_row_cursor.py new file mode 100644 index 0000000..b006036 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/data_table_row_cursor.py @@ -0,0 +1,36 @@ +import csv +import io + +from textual.app import App, ComposeResult +from textual_fastdatatable import ArrowBackend, DataTable + +CSV = """lane,swimmer,country,time +4,Joseph Schooling,Singapore,50.39 +2,Michael Phelps,United States,51.14 +5,Chad le Clos,South Africa,51.14 +6,László Cseh,Hungary,51.14 +3,Li Zhuhao,China,51.26 +8,Mehdy Metella,France,51.58 +7,Tom Shields,United States,51.73 +1,Aleksandr Sadovnikov,Russia,51.84""" + + +class TableApp(App): + def compose(self) -> ComposeResult: + rows = csv.reader(io.StringIO(CSV)) + labels = next(rows) + data = [row for row in rows] + backend = ArrowBackend.from_pydict( + {label: [row[i] for row in data] for i, label in enumerate(labels)} + ) + table = DataTable(backend=backend) + table.focus() + table.cursor_type = "row" + table.fixed_columns = 1 + table.fixed_rows = 1 + yield table + + +if __name__ == "__main__": + app = TableApp() + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/data_table_row_labels.py b/tests/snapshot_tests/snapshot_apps/data_table_row_labels.py new file mode 100644 index 0000000..e50e3cf --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/data_table_row_labels.py @@ -0,0 +1,37 @@ +from textual.app import App, ComposeResult +from textual_fastdatatable import DataTable + +ROWS = [ + ("lane", "swimmer", "country", "time"), + (5, "Chad le Clos", "South Africa", 51.14), + (4, "Joseph Schooling", "Singapore", 50.39), + (2, "Michael Phelps", "United States", 51.14), + (6, "László Cseh", "Hungary", 51.14), + (3, "Li Zhuhao", "China", 51.26), + (8, "Mehdy Metella", "France", 51.58), + (7, "Tom Shields", "United States", 51.73), + (10, "Darren Burns", "Scotland", 51.84), + (1, "Aleksandr Sadovnikov", "Russia", 51.84), +] + + +class TableApp(App): + def compose(self) -> ComposeResult: + yield DataTable() + + def on_mount(self) -> None: + table = self.query_one(DataTable) + table.fixed_rows = 1 + table.fixed_columns = 1 + table.focus() + rows = iter(ROWS) + column_labels = next(rows) + for column in column_labels: + table.add_column(column, key=column) + for index, row in enumerate(rows): + table.add_row(*row, label=str(index)) + + +app = TableApp() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/data_table_sort.py b/tests/snapshot_tests/snapshot_apps/data_table_sort.py new file mode 100644 index 0000000..8e45167 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/data_table_sort.py @@ -0,0 +1,40 @@ +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual_fastdatatable import ArrowBackend, DataTable + +# Shuffled around a bit to exercise sorting. +ROWS = [ + ("lane", "swimmer", "country", "time"), + (5, "Chad le Clos", "South Africa", 51.14), + (4, "Joseph Schooling", "Singapore", 50.39), + (2, "Michael Phelps", "United States", 51.14), + (6, "László Cseh", "Hungary", 51.14), + (3, "Li Zhuhao", "China", 51.26), + (8, "Mehdy Metella", "France", 51.58), + (7, "Tom Shields", "United States", 51.73), + (10, "Darren Burns", "Scotland", 51.84), + (1, "Aleksandr Sadovnikov", "Russia", 51.84), +] + + +class TableApp(App): + BINDINGS = [ + Binding("s", "sort", "Sort"), + ] + + def compose(self) -> ComposeResult: + backend = ArrowBackend.from_records(ROWS, has_header=True) + yield DataTable(backend=backend) + + def on_mount(self) -> None: + table = self.query_one(DataTable) + table.focus() + + def action_sort(self): + table = self.query_one(DataTable) + table.sort([("time", "ascending"), ("lane", "ascending")]) + + +app = TableApp() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/data_table_style_order.py b/tests/snapshot_tests/snapshot_apps/data_table_style_order.py new file mode 100644 index 0000000..a301b1d --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/data_table_style_order.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from textual.app import App, ComposeResult +from textual.widgets import Label +from textual_fastdatatable import ArrowBackend, DataTable +from typing_extensions import Literal + +data = [ + "Severance", + "Foundation", + "Dark", +] + + +def make_datatable( + foreground_priority: Literal["css", "renderable"], + background_priority: Literal["css", "renderable"], +) -> DataTable: + backend = ArrowBackend.from_pydict( + {"Movies": [f"[red on blue]{row}" for row in data]} + ) + table = DataTable( + backend=backend, + cursor_foreground_priority=foreground_priority, + cursor_background_priority=background_priority, + ) + table.zebra_stripes = True + return table + + +class DataTableCursorStyles(App): + """Regression test snapshot app which ensures that styles + are layered on top of each other correctly in the DataTable. + In this example, the colour of the text in the cells under + the cursor should not be red, because the CSS should be applied + on top.""" + + CSS = """ + DataTable {margin-bottom: 1;} + DataTable > .datatable--cursor { + color: $secondary; + background: $success; + text-style: bold italic; + } +""" + + def compose(self) -> ComposeResult: + priorities: list[ + tuple[Literal["css", "renderable"], Literal["css", "renderable"]] + ] = [ + ("css", "css"), + ("css", "renderable"), + ("renderable", "renderable"), + ("renderable", "css"), + ] + for foreground, background in priorities: + yield Label(f"Foreground is {foreground!r}, background is {background!r}:") + table = make_datatable(foreground, background) + yield table + + +app = DataTableCursorStyles() + +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/datatable_hot_reloading.py b/tests/snapshot_tests/snapshot_apps/datatable_hot_reloading.py new file mode 100644 index 0000000..451b368 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/datatable_hot_reloading.py @@ -0,0 +1,58 @@ +from pathlib import Path + +from textual.app import App, ComposeResult +from textual_fastdatatable import ArrowBackend, DataTable + +CSS_PATH = (Path(__file__) / "../datatable_hot_reloading.tcss").resolve() + +# Write some CSS to the file before the app loads. +# Then, the test will clear all the CSS to see if the +# hot reloading applies the changes correctly. +CSS_PATH.write_text( + """\ +DataTable > .datatable--cursor { + background: purple; +} + +DataTable > .datatable--fixed { + background: red; +} + +DataTable > .datatable--fixed-cursor { + background: blue; +} + +DataTable > .datatable--header { + background: yellow; +} + +DataTable > .datatable--odd-row { + background: pink; +} + +DataTable > .datatable--even-row { + background: brown; +} +""" +) + + +class DataTableHotReloadingApp(App[None]): + CSS_PATH = CSS_PATH + + def compose(self) -> ComposeResult: + data = { + # orig test set A width=10, we fake it with spaces + "A ": ["one", "three", "five"], + "B": ["two", "four", "six"], + } + backend = ArrowBackend.from_pydict(data) + yield DataTable(backend, zebra_stripes=True, cursor_type="row", fixed_columns=1) + + def on_mount(self) -> None: + self.query_one(DataTable) + + +if __name__ == "__main__": + app = DataTableHotReloadingApp() + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/datatable_hot_reloading.tcss b/tests/snapshot_tests/snapshot_apps/datatable_hot_reloading.tcss new file mode 100644 index 0000000..5e9ee82 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/datatable_hot_reloading.tcss @@ -0,0 +1 @@ +/* This file is purposefully empty. */ diff --git a/tests/snapshot_tests/snapshot_apps/empty.py b/tests/snapshot_tests/snapshot_apps/empty.py new file mode 100644 index 0000000..0d42bc1 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/empty.py @@ -0,0 +1,12 @@ +from textual.app import App, ComposeResult +from textual_fastdatatable import DataTable + + +class TableApp(App): + def compose(self) -> ComposeResult: + yield DataTable() + + +app = TableApp() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/empty_add_col.py b/tests/snapshot_tests/snapshot_apps/empty_add_col.py new file mode 100644 index 0000000..d6fdc7b --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/empty_add_col.py @@ -0,0 +1,17 @@ +from textual.app import App, ComposeResult +from textual_fastdatatable import DataTable + + +class TableApp(App): + def compose(self) -> ComposeResult: + yield DataTable() + + def on_mount(self) -> None: + table = self.query_one(DataTable) + table.add_column("Foo") + table.add_rows([("1",), ("2",)]) + + +app = TableApp() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/from_parquet.py b/tests/snapshot_tests/snapshot_apps/from_parquet.py new file mode 100644 index 0000000..f64d700 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/from_parquet.py @@ -0,0 +1,16 @@ +from pathlib import Path + +from textual.app import App, ComposeResult +from textual_fastdatatable import DataTable + + +class TableApp(App): + def compose(self) -> ComposeResult: + yield DataTable( + data=Path(__file__).parent.parent.parent / "data" / "lap_times_100.parquet" + ) + + +app = TableApp() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/from_pydict_with_col_labels.py b/tests/snapshot_tests/snapshot_apps/from_pydict_with_col_labels.py new file mode 100644 index 0000000..343fdce --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/from_pydict_with_col_labels.py @@ -0,0 +1,20 @@ +from textual.app import App, ComposeResult +from textual_fastdatatable import DataTable + +DATA = { + "Foo": list(range(50)), + "Bar": ["0123456789"] * 50, + "Baz": ["IJKLMNOPQRSTUVWXYZ"] * 50, +} + + +class TableApp(App): + def compose(self) -> ComposeResult: + yield DataTable( + data=DATA, column_labels=["[red]Not Foo[/red]", "Zig", "[reverse]Zag[/]"] + ) + + +app = TableApp() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/from_records.py b/tests/snapshot_tests/snapshot_apps/from_records.py new file mode 100644 index 0000000..8650dce --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/from_records.py @@ -0,0 +1,25 @@ +from textual.app import App, ComposeResult +from textual_fastdatatable import DataTable + +ROWS = [ + ("lane", "swimmer", "country", "time"), + (4, "Joseph Schooling", "Singapore", 50.39), + (2, "Michael Phelps", "United States", 51.14), + (5, "Chad le Clos", "South Africa", 51.14), + (6, "László Cseh", "Hungary", 51.14), + (3, "Li Zhuhao", "China", 51.26), + (8, "Mehdy Metella", "France", 51.58), + (7, "Tom Shields", "United States", 51.73), + (1, "Aleksandr Sadovnikov", "Russia", 51.84), + (10, "Darren Burns", "Scotland", 51.84), +] + + +class TableApp(App): + def compose(self) -> ComposeResult: + yield DataTable(data=ROWS) + + +app = TableApp() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/no_rows.py b/tests/snapshot_tests/snapshot_apps/no_rows.py new file mode 100644 index 0000000..fd99b30 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/no_rows.py @@ -0,0 +1,12 @@ +from textual.app import App, ComposeResult +from textual_fastdatatable import DataTable + + +class TableApp(App): + def compose(self) -> ComposeResult: + yield DataTable(column_labels=["foo [red]foo[/red]", "bar"]) + + +app = TableApp() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/no_rows_empty_sequence.py b/tests/snapshot_tests/snapshot_apps/no_rows_empty_sequence.py new file mode 100644 index 0000000..fd99b30 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/no_rows_empty_sequence.py @@ -0,0 +1,12 @@ +from textual.app import App, ComposeResult +from textual_fastdatatable import DataTable + + +class TableApp(App): + def compose(self) -> ComposeResult: + yield DataTable(column_labels=["foo [red]foo[/red]", "bar"]) + + +app = TableApp() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py new file mode 100644 index 0000000..ad8a235 --- /dev/null +++ b/tests/snapshot_tests/test_snapshots.py @@ -0,0 +1,119 @@ +from pathlib import Path +from typing import Callable + +import pytest + +# These paths should be relative to THIS directory. +SNAPSHOT_APPS_DIR = Path("./snapshot_apps") + + +def test_auto_table(snap_compare: Callable) -> None: + assert snap_compare(SNAPSHOT_APPS_DIR / "auto-table.py", terminal_size=(120, 40)) + + +def test_datatable_render(snap_compare: Callable) -> None: + press = ["down", "down", "right", "up", "left"] + assert snap_compare(SNAPSHOT_APPS_DIR / "data_table.py", press=press) + + +def test_datatable_row_cursor_render(snap_compare: Callable) -> None: + press = ["up", "left", "right", "down", "down"] + assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_row_cursor.py", press=press) + + +def test_datatable_no_render_markup(snap_compare: Callable) -> None: + assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_no_render_markup.py") + + +def test_datatable_null_mixed_cols(snap_compare: Callable) -> None: + assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_null_mixed_cols.py") + + +def test_datatable_range_cursor_render(snap_compare: Callable) -> None: + press = ["right", "down", "shift+right", "shift+down", "shift+down"] + assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_range_cursor.py", press=press) + + +def test_datatable_column_cursor_render(snap_compare: Callable) -> None: + press = ["left", "up", "down", "right", "right"] + assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_column_cursor.py", press=press) + + +def test_datatable_max_width_render(snap_compare: Callable) -> None: + press = ["right", "down", "shift+right", "shift+down", "shift+down"] + assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_max_width.py", press=press) + + +def test_datatable_sort_multikey(snap_compare: Callable) -> None: + press = ["down", "right", "s"] # Also checks that sort doesn't move cursor. + assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_sort.py", press=press) + + +def test_datatable_remove_row(snap_compare: Callable) -> None: + press = ["r"] + assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_remove_row.py", press=press) + + +@pytest.mark.skip(reason="Don't support row labels.") +def test_datatable_labels_and_fixed_data(snap_compare: Callable) -> None: + # Ensure that we render correctly when there are fixed rows/cols and labels. + assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_row_labels.py") + + +# skip, don't xfail; see: https://github.com/Textualize/pytest-textual-snapshot/issues/6 +@pytest.mark.skip( + reason=( + "The data in this test includes markup; the backend doesn't" + "know these have zero width, so we draw the column wider than we used to" + ) +) +def test_datatable_style_ordering(snap_compare: Callable) -> None: + # Regression test for https -> None://github.com/Textualize/textual/issues/2061 + assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_style_order.py") + + +def test_datatable_add_column(snap_compare: Callable) -> None: + # Checking adding columns after adding rows + assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_add_column.py") + + +@pytest.mark.skip(reason="No multi-height rows. No Rich objects.") +def test_datatable_add_row_auto_height(snap_compare: Callable) -> None: + # Check that rows added with auto height computation look right. + assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_add_row_auto_height.py") + + +@pytest.mark.skip(reason="No multi-height rows. No Rich objects.") +def test_datatable_add_row_auto_height_sorted(snap_compare: Callable) -> None: + # Check that rows added with auto height computation look right. + assert snap_compare( + SNAPSHOT_APPS_DIR / "data_table_add_row_auto_height.py", press=["s"] + ) + + +def test_datatable_empty(snap_compare: Callable) -> None: + assert snap_compare(SNAPSHOT_APPS_DIR / "empty.py") + + +def test_datatable_empty_add_col(snap_compare: Callable) -> None: + assert snap_compare(SNAPSHOT_APPS_DIR / "empty_add_col.py") + + +def test_datatable_no_rows(snap_compare: Callable) -> None: + assert snap_compare(SNAPSHOT_APPS_DIR / "no_rows.py") + + +def test_datatable_no_rows_empty_sequence(snap_compare: Callable) -> None: + assert snap_compare(SNAPSHOT_APPS_DIR / "no_rows_empty_sequence.py") + + +def test_datatable_from_parquet(snap_compare: Callable) -> None: + assert snap_compare(SNAPSHOT_APPS_DIR / "from_parquet.py") + + +def test_datatable_from_records(snap_compare: Callable) -> None: + assert snap_compare(SNAPSHOT_APPS_DIR / "from_records.py") + + +def test_datatable_from_pydict(snap_compare: Callable) -> None: + assert snap_compare(SNAPSHOT_APPS_DIR / "from_pydict_with_col_labels.py") diff --git a/tests/unit_tests/test_arrow_backend.py b/tests/unit_tests/test_arrow_backend.py new file mode 100644 index 0000000..661b7bb --- /dev/null +++ b/tests/unit_tests/test_arrow_backend.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Sequence + +import pyarrow as pa +from textual_fastdatatable import ArrowBackend + + +def test_from_records(records: list[tuple[str | int, ...]]) -> None: + backend = ArrowBackend.from_records(records, has_header=True) + assert backend.column_count == 3 + assert backend.row_count == 5 + assert tuple(backend.columns) == records[0] + + +def test_from_records_no_header(records: list[tuple[str | int, ...]]) -> None: + backend = ArrowBackend.from_records(records[1:], has_header=False) + assert backend.column_count == 3 + assert backend.row_count == 5 + assert tuple(backend.columns) == ("f0", "f1", "f2") + + +def test_from_pydict(pydict: dict[str, Sequence[str | int]]) -> None: + backend = ArrowBackend.from_pydict(pydict) + assert backend.column_count == 3 + assert backend.row_count == 5 + assert backend.source_row_count == 5 + assert tuple(backend.columns) == tuple(pydict.keys()) + + +def test_from_pydict_with_limit(pydict: dict[str, Sequence[str | int]]) -> None: + backend = ArrowBackend.from_pydict(pydict, max_rows=2) + assert backend.column_count == 3 + assert backend.row_count == 2 + assert backend.source_row_count == 5 + assert tuple(backend.columns) == tuple(pydict.keys()) + + +def test_from_parquet(pydict: dict[str, Sequence[str | int]], tmp_path: Path) -> None: + tbl = pa.Table.from_pydict(pydict) + p = tmp_path / "test.parquet" + pa.parquet.write_table(tbl, str(p)) + + backend = ArrowBackend.from_parquet(p) + assert backend.data.equals(tbl) + + +def test_empty_query() -> None: + data: dict[str, list] = {"a": []} + backend = ArrowBackend.from_pydict(data) + assert backend.column_content_widths == [0] + + +def test_dupe_column_labels() -> None: + arr = pa.array([0, 1, 2, 3]) + tab = pa.table([arr] * 3, names=["a", "a", "a"]) + backend = ArrowBackend(data=tab) + assert backend.column_count == 3 + assert backend.row_count == 4 + assert backend.get_row_at(2) == [2, 2, 2] + + +def test_timestamp_with_tz() -> None: + """ + Ensure datetimes with offsets but no names do not crash the data table + when casting to string. + """ + dt = datetime(2024, 1, 1, hour=15, tzinfo=timezone(offset=timedelta(hours=-5))) + arr = pa.array([dt, dt, dt]) + tab = pa.table([arr], names=["created_at"]) + backend = ArrowBackend(data=tab) + assert backend.column_content_widths == [29] + + +def test_mixed_types() -> None: + data = [(1000,), ("hi",)] + backend = ArrowBackend.from_records(records=data) + assert backend + assert backend.row_count == 2 + assert backend.get_row_at(0) == ["1000"] + assert backend.get_row_at(1) == ["hi"] + + +def test_negative_timestamps() -> None: + dt = datetime(1, 1, 1, tzinfo=timezone.utc) + arr = pa.array([dt, dt, dt], type=pa.timestamp("s", tz="America/New_York")) + tab = pa.table([arr], names=["created_at"]) + backend = ArrowBackend(data=tab) + assert backend.column_content_widths == [26] + assert backend.get_column_at(0) == [datetime.min, datetime.min, datetime.min] + assert backend.get_row_at(0) == [datetime.min] + assert backend.get_cell_at(0, 0) is datetime.min diff --git a/tests/unit_tests/test_backends.py b/tests/unit_tests/test_backends.py new file mode 100644 index 0000000..bdac451 --- /dev/null +++ b/tests/unit_tests/test_backends.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +import pytest +from textual_fastdatatable.backend import DataTableBackend + + +def test_column_content_widths(backend: DataTableBackend) -> None: + assert backend.column_content_widths == [1, 8, 6] + + +def test_get_row_at(backend: DataTableBackend) -> None: + assert backend.get_row_at(0) == [1, "a", "foo"] + assert backend.get_row_at(4) == [5, "asdfasdf", "foofoo"] + with pytest.raises(IndexError): + backend.get_row_at(10) + with pytest.raises(IndexError): + backend.get_row_at(-1) + + +def test_get_column_at(backend: DataTableBackend) -> None: + assert backend.get_column_at(0) == [1, 2, 3, 4, 5] + assert backend.get_column_at(2) == ["foo", "bar", "baz", "qux", "foofoo"] + + with pytest.raises(IndexError): + backend.get_column_at(10) + + +def test_get_cell_at(backend: DataTableBackend) -> None: + assert backend.get_cell_at(0, 0) == 1 + assert backend.get_cell_at(4, 1) == "asdfasdf" + with pytest.raises(IndexError): + backend.get_cell_at(10, 0) + with pytest.raises(IndexError): + backend.get_cell_at(0, 10) + + +def test_append_column(backend: DataTableBackend) -> None: + original_table = backend.data + backend.append_column("new") + assert backend.column_count == 4 + assert backend.row_count == 5 + assert backend.get_column_at(3) == [None] * backend.row_count + + backend.append_column("def", default="zzz") + assert backend.column_count == 5 + assert backend.row_count == 5 + assert backend.get_column_at(4) == ["zzz"] * backend.row_count + + assert backend.data.select(["first column", "two", "three"]).equals(original_table) + + +def test_append_rows(backend: DataTableBackend) -> None: + original_table = backend.data + backend.append_rows([(6, "w", "x"), (7, "y", "z")]) + assert backend.column_count == 3 + assert backend.row_count == 7 + assert backend.column_content_widths == [1, 8, 6] + + backend.append_rows([(999, "w" * 12, "x" * 15)]) + assert backend.column_count == 3 + assert backend.row_count == 8 + assert backend.column_content_widths == [3, 12, 15] + + assert backend.data.slice(0, 5).equals(original_table) + + +def test_drop_row(backend: DataTableBackend) -> None: + backend.drop_row(0) + assert backend.row_count == 4 + assert backend.column_count == 3 + assert backend.column_content_widths == [1, 8, 6] + + backend.drop_row(3) + assert backend.row_count == 3 + assert backend.column_count == 3 + assert backend.column_content_widths == [1, 1, 3] + + with pytest.raises(IndexError): + backend.drop_row(3) + + +def test_update_cell(backend: DataTableBackend) -> None: + backend.update_cell(0, 0, 0) + assert backend.get_column_at(0) == [0, 2, 3, 4, 5] + assert backend.row_count == 5 + assert backend.column_count == 3 + assert backend.column_content_widths == [1, 8, 6] + + backend.update_cell(3, 1, "z" * 50) + assert backend.get_row_at(3) == [4, "z" * 50, "qux"] + assert backend.row_count == 5 + assert backend.column_count == 3 + assert backend.column_content_widths == [1, 50, 6] + + +def test_sort(backend: DataTableBackend) -> None: + original_table = backend.data + original_col_one = list(backend.get_column_at(0)).copy() + original_col_two = list(backend.get_column_at(1)).copy() + backend.sort(by="two") + assert backend.get_column_at(0) != original_col_one + assert backend.get_column_at(1) == sorted(original_col_two) + + backend.sort(by=[("two", "descending")]) + assert backend.get_column_at(0) != original_col_one + assert backend.get_column_at(1) == sorted(original_col_two, reverse=True) + + backend.sort(by=[("first column", "ascending")]) + assert backend.data.equals(original_table) diff --git a/tests/unit_tests/test_create_backend.py b/tests/unit_tests/test_create_backend.py new file mode 100644 index 0000000..49b4cf8 --- /dev/null +++ b/tests/unit_tests/test_create_backend.py @@ -0,0 +1,54 @@ +from datetime import date, datetime + +import pyarrow as pa +from textual_fastdatatable.backend import create_backend + +MAX_32BIT_INT = 2**31 - 1 +MAX_64BIT_INT = 2**63 - 1 + + +def test_empty_sequence() -> None: + backend = create_backend(data=[]) + assert backend + assert backend.row_count == 0 + assert backend.column_count == 0 + assert backend.columns == [] + assert backend.column_content_widths == [] + + +def test_infinity_timestamps() -> None: + from_py = create_backend( + data={"dt": [date.max, date.min], "ts": [datetime.max, datetime.min]} + ) + assert from_py + assert from_py.row_count == 2 + + from_arrow = create_backend( + data=pa.table( + { + "dt32": [ + pa.scalar(MAX_32BIT_INT, type=pa.date32()), + pa.scalar(-MAX_32BIT_INT, type=pa.date32()), + ], + "dt64": [ + pa.scalar(MAX_64BIT_INT, type=pa.date64()), + pa.scalar(-MAX_64BIT_INT, type=pa.date64()), + ], + "ts": [ + pa.scalar(MAX_64BIT_INT, type=pa.timestamp("s")), + pa.scalar(-MAX_64BIT_INT, type=pa.timestamp("s")), + ], + "tns": [ + pa.scalar(MAX_64BIT_INT, type=pa.timestamp("ns")), + pa.scalar(-MAX_64BIT_INT, type=pa.timestamp("ns")), + ], + } + ) + ) + assert from_arrow + assert from_arrow.row_count == 2 + assert from_arrow.get_row_at(0) == [date.max, date.max, datetime.max, datetime.max] + assert from_arrow.get_row_at(1) == [date.min, date.min, datetime.min, datetime.min] + assert from_arrow.get_column_at(0) == [date.max, date.min] + assert from_arrow.get_column_at(2) == [datetime.max, datetime.min] + assert from_arrow.get_cell_at(0, 0) == date.max