diff --git a/.copier-answers.yml b/.copier-answers.yml new file mode 100644 index 0000000..4a963cb --- /dev/null +++ b/.copier-answers.yml @@ -0,0 +1,12 @@ +# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY +_commit: 22c0aa6abc4acb083663b817bb58202193e9c3ef +_src_path: gh:ONEcampaign/bblocks-projects +project_name: oda_reader +project_slug: oda_reader +project_description: A simple package to import ODA data from the OECD's API and AidData's database +project_type: package +author_name: Jorge Rivera +author_email: jorge.rivera@one.org +python_version: "3.11" +license: mit +github_username: ONEcampaign diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d69e00b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,87 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lint: + name: Lint & type check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v8.1.0 + + - name: Cache uv + uses: actions/cache@v5 + with: + path: ~/.cache/uv + key: ${{ runner.os }}-uv-${{ hashFiles('uv.lock') }} + restore-keys: | + ${{ runner.os }}-uv- + + - name: Set up Python 3.11 + run: uv python install '3.11' + + - name: Install dependencies + run: uv sync --frozen --all-groups + + - name: Run ruff check + run: uv run ruff check . + + - name: Run ruff format check + run: uv run ruff format --check . + + - name: Run ty + run: uv run ty check src/oda_reader + + test: + name: Run tests + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ["3.11", "3.12", "3.13"] + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v8.1.0 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache uv + uses: actions/cache@v5 + with: + path: ~/.cache/uv + key: ${{ runner.os }}-uv-${{ hashFiles('uv.lock') }} + restore-keys: | + ${{ runner.os }}-uv- + + - name: Install dependencies + run: uv sync --all-groups + + - name: Run unit tests + run: uv run pytest tests/ -n auto -m "not integration" --cov=src/oda_reader --cov-report=xml --cov-report=term + env: + ODA_READER_CACHE_DIR: ${{ runner.temp }}/oda_cache + + - name: Integration tests + if: github.event_name == 'pull_request' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-latest' + run: uv run pytest tests/ -v + env: + ODA_READER_CACHE_DIR: ${{ runner.temp }}/oda_cache + + - name: Upload coverage + uses: codecov/codecov-action@v6 + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' + with: + files: ./coverage.xml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f1271d7..8b47798 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,44 +1,58 @@ -name: release +name: Release on: push: tags: - 'v*' - workflow_dispatch: + +permissions: + contents: read + id-token: write # Required for PyPI trusted publishing jobs: - release: - name: Create Release + build: runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 - strategy: - matrix: - python-versions: ["3.13"] + - name: Install uv + uses: astral-sh/setup-uv@v8.1.0 - permissions: - contents: write # needed by action-gh-release + - name: Cache uv + uses: actions/cache@v5 + with: + path: ~/.cache/uv + key: ${{ runner.os }}-uv-${{ hashFiles('uv.lock') }} + restore-keys: | + ${{ runner.os }}-uv- - steps: - - name: Checkout - uses: actions/checkout@v4 + - name: Set up Python 3.11 + run: uv python install '3.11' - - name: Extract version from tag - id: tag_name - run: echo "current_version=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" - shell: bash + - name: Build package + run: uv build --no-sources - - name: Set up uv (and Python) - uses: astral-sh/setup-uv@v6 + - name: Upload build artifacts + uses: actions/upload-artifact@v7 with: - python-version: ${{ matrix.python-versions }} + name: dist + path: dist/ - - name: Build sdist and wheel (uv) - run: uv build --no-sources + publish: + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/oda_reader + steps: + - name: Install uv + uses: astral-sh/setup-uv@v8.1.0 - - name: Show dist/ - run: ls -l dist + - name: Download build artifacts + uses: actions/download-artifact@v8 + with: + name: dist + path: dist/ - - name: Publish to PyPI (uv) - env: - UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }} + - name: Publish to PyPI run: uv publish diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 61fc62b..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Tests - -on: - pull_request: - branches: [ main] - -jobs: - test: - name: Run Tests - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.11", "3.12", "3.13"] - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up uv - uses: astral-sh/setup-uv@v6 - with: - python-version: ${{ matrix.python-version }} - enable-cache: false - - - name: Install dependencies - run: uv sync --all-groups - - - name: Run unit tests - run: uv run pytest tests/ -n auto -m "not integration" -v - env: - ODA_READER_CACHE_DIR: ${{ runner.temp }}/oda_cache - - - name: Integration Tests - if: github.event_name == 'pull_request' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-latest' - run: uv run pytest tests/ -v - env: - ODA_READER_CACHE_DIR: ${{ runner.temp }}/oda_cache diff --git a/.gitignore b/.gitignore index b07b43d..e716e2e 100644 --- a/.gitignore +++ b/.gitignore @@ -162,3 +162,11 @@ cython_debug/ /src/oda_reader/dev_tests.py /docs/site /docs/plans + +# Ruff +.ruff_cache/ + +# AI assistants +.claude/ +CLAUDE.md +AGENTS.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5bd0faf..0bed960 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,119 +1,119 @@ -# Pre-commit hooks for ODA Reader Package +# Pre-commit hooks for ODA Reader (oda_reader) # # Installation: -# 1. Install pre-commit: `uv sync --group dev` -# 2. Install hooks: `uv run pre-commit install` +# 1. Install dev deps: uv sync --group dev +# 2. Enable hooks: uv run pre-commit install # # Usage: -# - Hooks run automatically on `git commit` -# - Run manually on all files: `uv run pre-commit run --all-files` -# - Run on specific files: `uv run pre-commit run --files src/file.py` -# - Update hooks: `uv run pre-commit autoupdate` -# - Skip hooks temporarily: `git commit --no-verify` +# - Run all files: uv run pre-commit run --all-files +# - Update hooks: uv run pre-commit autoupdate +# - Skip once: git commit --no-verify repos: - # ============================================================================ - # Ruff - Fast Python linter and formatter (replaces black, isort, flake8) - # ============================================================================ + # --------------------------------------------------------------------------- + # Ruff: fast linter + formatter (replaces black, isort, flake8) + # --------------------------------------------------------------------------- - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.4 + rev: v0.15.12 hooks: - # Run the linter - - id: ruff + - id: ruff-check args: [--fix] - # Run the formatter - id: ruff-format - # ============================================================================ - # Built-in pre-commit hooks for file quality - # ============================================================================ + # --------------------------------------------------------------------------- + # Core hygiene, JSON/YAML/TOML, security + # --------------------------------------------------------------------------- - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - # Remove trailing whitespace + # Whitespace & endings - id: trailing-whitespace args: [--markdown-linebreak-ext=md] - - # Ensure files end with a newline - id: end-of-file-fixer - - # Prevent mixed line endings (Unix vs Windows) - id: mixed-line-ending args: [--fix=lf] - # Check for files that would conflict on case-insensitive filesystems + # Conflicts & platform quirks - id: check-case-conflict - - # Check for merge conflict strings - id: check-merge-conflict - # Check for debug statements (pdb, breakpoint, etc.) + # Python sanity - id: debug-statements - - # Validate Python syntax - id: check-ast - - # Check for proper shebang formatting - id: check-shebang-scripts-are-executable + - id: check-builtin-literals + - id: check-docstring-first - # Prevent large files from being committed (default 500kb) - - id: check-added-large-files - args: [--maxkb=1000] # Allow up to 1MB files - exclude: | - (?x)^( - .*\.parquet| - .*\.feather| - uv\.lock - )$ - - # ============================================================================ - # JSON validation (critical for schema mappings) - # ============================================================================ - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 - hooks: - # Check JSON syntax + # Config/data formats - id: check-json - - # Pretty-format JSON files - id: pretty-format-json args: [--autofix, --indent=2, --no-sort-keys] exclude: | (?x)^( - \.vscode/.*| - \.claude/.* + \.idea/.*| + \.vscode/.*| + \.claude/.* )$ - - # ============================================================================ - # YAML validation (for GitHub Actions and MkDocs) - # ============================================================================ - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 - hooks: - # Check YAML syntax - id: check-yaml args: [--allow-multiple-documents] + - id: check-toml - # ============================================================================ - # Security checks - # ============================================================================ - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 - hooks: - # Detect private keys + # Basic secret hygiene - id: detect-private-key - # ============================================================================ - # Python-specific checks - # ============================================================================ - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + # Large files (5MB) with sensible excludes + - id: check-added-large-files + args: [--maxkb=5000] + exclude: | + (?x)^( + .*\.parquet| + .*\.feather| + uv\.lock + )$ + + # --------------------------------------------------------------------------- + # Type checking (uses settings from pyproject.toml) + # --------------------------------------------------------------------------- + - repo: local hooks: - # Check for common Python mistakes - - id: check-builtin-literals + - id: ty + name: ty type check + language: system + entry: uv run ty check + types: [python] + pass_filenames: false + + # --------------------------------------------------------------------------- + # Spelling (docs + code comments) + # --------------------------------------------------------------------------- + - repo: https://github.com/codespell-project/codespell + rev: v2.4.2 + hooks: + - id: codespell + args: [--write-changes] + exclude: | + (?x)^( + .*\.lock| + .*\.parquet| + .*\.feather| + .*\.json + )$ - # Check docstring is first - - id: check-docstring-first + # --------------------------------------------------------------------------- + # GitHub Actions linting + # --------------------------------------------------------------------------- + - repo: https://github.com/rhysd/actionlint + rev: v1.7.12 + hooks: + - id: actionlint - # Validate pyproject.toml - - id: check-toml + # --------------------------------------------------------------------------- + # Markdown formatting (GFM + front matter) + # --------------------------------------------------------------------------- + - repo: https://github.com/hukkin/mdformat + rev: 1.0.0 + hooks: + - id: mdformat + additional_dependencies: + - mdformat-gfm + - mdformat-frontmatter diff --git a/CHANGELOG.md b/CHANGELOG.md index bf56b27..4b737cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,19 @@ # Changelog for oda_reader +## Unreleased + +- Project maintenance: adopted the [`bblocks-projects`](https://github.com/ONEcampaign/bblocks-projects) + template standard so the repo is now managed (`bblocks-projects update` / `doctor` work via + `.copier-answers.yml`). Adds the `ty` type checker (enforced in CI and pre-commit) and full + public-API type annotations, expands ruff rules (annotations, perflint, eradicate), refreshes + pre-commit hooks (codespell, actionlint, mdformat, ty), and adds a `py.typed` marker so type + information ships with the wheel. +- **Minimum supported Python is now 3.11** (was 3.10). Python 3.10 reaches end-of-life in + October 2026. +- CI release now uses PyPI trusted publishing (OIDC) instead of an API token. + ## 1.6.0 (2026-04-28) + - Adds `use_raw_cache=False` to `bulk_download_crs`, `download_crs_file`, `bulk_download_dac2a` and `bulk_download_multisystem` for the cases where you want to bypass the bulk cache and re-download fresh on every call. Caching remains on by default. @@ -26,25 +39,31 @@ - Cached archives are validated with `zipfile.is_zipfile` before reuse; a corrupt entry is removed and re-downloaded transparently. ## 1.5.1 (2026-04-15) + - Adds support for Deflate64-compressed ZIP files in bulk downloads. The OECD switched the full CRS bulk file to Deflate64 compression, which Python's standard library does not support. This release patches `zipfile` at runtime using the `inflate64` library to handle Deflate64 transparently. - Adds `inflate64` as a dependency. ## 1.5.0 (2026-04-09) + - Replaces blind version-decrement fallback with authoritative SDMX metadata endpoint lookup for all datasets. - Adds `clear_version_cache()` to the public API for forcing fresh version discovery mid-session. ## 1.4.3 (2026-04-09) + - Fixes DAC1 query filter dimension order to match the current DSD schema, which added SECTOR at position 2. - Bumps DAC1 dataflow version from 1.7 to 1.8. ## 1.4.2 (2026-01-23) + - Updates DAC1 dataflow version from 1.5 to 1.7. - Updates DAC2a dataflow version from 1.6 to 1.4. ## 1.4.1 (2025-12-19) + - Extends bulk download auto-detection to support `.csv` files in addition to `.txt` files. ## 1.4.0 (2025-12-19) + - Adds `bulk_download_dac2a()` function for bulk downloading the full DAC2A dataset. - Auto-detects file types (parquet or txt) in bulk downloads, removing the need for the `is_txt` parameter. - Auto-detects CSV delimiters (comma, pipe, tab, semicolon) when reading txt files from bulk downloads. @@ -52,80 +71,100 @@ - Adds pytest and pytest-mock to dev dependencies for improved testing support. ## 1.3.5 (2025-12-19) + - Fixes `_get_dataflow_version()` to gracefully handle URLs without a version pattern instead of crashing. ## 1.3.4 (2025-12-19) + - Improves robustness of dataflow version fallback logic. The API error detection now checks response content regardless of HTTP status code, handling cases where error messages like "Could not find Dataflow" are returned with various status codes. ## 1.3.3 (2025-12-19) + - Reverts DAC1 dataflow version from 1.6 to 1.5 to ensure compatibility with published data. ## 1.3.2 (2025-12-19) + - Updates bulk file dataflow version to 1.6 to match OECD's latest schema. ## 1.3.1 (2025-06-27) + - Improves cache management for very large files. Introduces tests and improved documentation ## 1.3.0 (2025-06-16) + - Improves cache management. ## 1.2.2 (2025-06-16) + - Fixes a bug with AidData where passing None for end year would raise -a type error. + a type error. ## 1.2.1 (2025-06-10) + - Introduces rate limiting (20 requests per minute).This is to better manage the OECD's aggressive rate throttling (20 requests per minute). ## 1.2.0 (2025-06-05) + - Introduces importer tools for the AidData's Global Chinese development finance dataset. - Introduces functionality to stream bulk files to avoid loading too much data to memory ## 1.1.5 (2025-05-15) + - The OECD has unexpectedly (and quietly) changed the naming convention -for the bulk Multisystem data. This is a small fix to address that. + for the bulk Multisystem data. This is a small fix to address that. ## 1.1.4 (2025-04-22) + - fix small cache bug ## 1.1.3 (2025-04-22) + - Small caching improvements ## 1.1.2 (2025-04-22) + - Extends caching to bulk downloaded files. - Other minor tweaks to how caching works. ## 1.1.1 (2025-04-16) -- Manages an issue created by the OECD when they are about to release new data. In that case -certain dataflows return `NoRecordsFound`, even though the query is valid for lower dataflows. -This version of `oda_reader` defends against that. +- Manages an issue created by the OECD when they are about to release new data. In that case + certain dataflows return `NoRecordsFound`, even though the query is valid for lower dataflows. + This version of `oda_reader` defends against that. ## 1.1.0 (2025-04-9) + - Introduces configurable and persistent caching via `joblib`. By default, the reader will keep a cache on disk (up to 1GB, for up to 7 days). This is to better manage the OECD's aggressive rate throttling (20 requests per minute). Entries older than 7 days are automatically cleared and `clear_cache` can be used to manually clear it. - ## 1.0.6 (2025-02-15) + - Improves warnings for duplicates on the multisystem dataset ## 1.0.5 (2025-02-15) + - Improves automatic handling of dataflows. ## 1.0.4 (2025-02-15) + - Improves automatic handling of DE CRS data. ## 1.0.3 (2025-02-15) + - Improves automatic handling of DE CRS data. ## 1.0.2 (2025-01-06) + - Improves automatic handling of dataflows when a dataflow exists but has no data. ## 1.0.1 (2025-01-06) + - Improves automatic handling of dataflows ## 1.0.0 (2024-10-06) + - Major release marking version 1.0.0. - Adds API support for the CRS and Multisystem datasets. - Adds support for bulk downloading of the CRS and Multisystem datasets in parquet format. @@ -134,24 +173,28 @@ This version of `oda_reader` defends against that. - General codebase improvements and documentation updates. ## 0.2.3 (2024-09-16) + - Fixes an error returned when making an API call to DAC1 without specifying the dataflow version. - An option to specify the dataflow version is now provided - This release pins dac1 dataflow to 1.2 ## 0.2.2 (2024-06-28) + - The schema provided by the OECD identifies the EU institutions under a code with no data. This update matches the right new code to the old 918. - The schema provided by the OECD does not correctly identify the donor code for DAC EU countries + EU Institutions. This update correctly matches it. ## 0.2.1 (2024-05-05) -- Allows for direct imports of `download_dac1`, `download_dac2a` and `QueryBuilder` as -`from oda_reader import download_dac1, download_dac2a, QueryBuilder`. +- Allows for direct imports of `download_dac1`, `download_dac2a` and `QueryBuilder` as + `from oda_reader import download_dac1, download_dac2a, QueryBuilder`. ## 0.2.0 (2024-05-05) + - Fixes a bug with `download_dac2a` which meant filters were not applied properly -and the wrong schema (dac1) was loaded. + and the wrong schema (dac1) was loaded. - Added a new method to the query builder to generate a dac2a filter expression. ## 0.1.0 (2024-05-05) + - Initial release. It includes a basic implementation of an API call for DAC1 and DAC2. - This release includes tools to translate the API response into the old .Stat schema. diff --git a/README.md b/README.md index 8c3a340..724541e 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) # ODA Reader + The OECD DAC Data Importer **ODA Reader** is a Python package that simplifies access to the **OECD DAC data**, leveraging @@ -19,15 +20,15 @@ ODA Reader is a project created and maintained by The ONE Campaign. ## Table of Contents 1. [Getting Started](#getting-started) -2. [Features](#features) -3. [Installation](#installation) -4. [DAC1](#downloading-dac1-data) -5. [DAC2a](#downloading-dac2a-data) -6. [CRS](#downloading-crs-data) -7. [Multisystem](#downloading-multisystem-data) -8. [Using filters](#using-filters) -9. [Rate limiting](#rate-limiting) -10. [Contribute](#contributing-to-oda-reader) +1. [Features](#features) +1. [Installation](#installation) +1. [DAC1](#downloading-dac1-data) +1. [DAC2a](#downloading-dac2a-data) +1. [CRS](#downloading-crs-data) +1. [Multisystem](#downloading-multisystem-data) +1. [Using filters](#using-filters) +1. [Rate limiting](#rate-limiting) +1. [Contribute](#contributing-to-oda-reader) ## Getting Started @@ -325,9 +326,9 @@ The `bulk_download_crs()` function allows you to download the full CRS data (as It accepts a few different arguments: - `save_to_path`: A string or `Path` object specifying a folder where the parquet file should be -saved. If not provided, `bulk_download_crs` will return a Pandas DataFrame. + saved. If not provided, `bulk_download_crs` will return a Pandas DataFrame. - `reduced_version`: A boolean which defaults to `False`. If `True` smaller file (removing certain -columns) is downloaded and saved/returned instead. + columns) is downloaded and saved/returned instead. - `as_iterator`: If `True` the function yields one `DataFrame` per row group instead of returning the entire file at once. This greatly reduces the peak memory usage when working with very large files. @@ -369,6 +370,7 @@ bulk_download_crs(save_to_path="./example-folder/", reduced_version=True) ``` To keep the smaller file in memory as a Pandas DataFrame: + ```python from oda_reader import bulk_download_crs @@ -393,6 +395,7 @@ download_crs_file(year=2022, save_to_path="./example-folder/") ``` For older years, the years are grouped in a single file. For example: + - 2004-05 - 2002-03 - 2000-01 @@ -416,7 +419,6 @@ from oda_reader import download_crs_file crs_data = download_crs_file(year=2017) ``` - ### Downloading Multisystem Data The `download_multisystem()` function allows you to download _Members total use of the @@ -536,6 +538,7 @@ full_multisystem = bulk_download_multisystem() ``` ## Using filters + When using ODA Reader, you can apply filters to refine the data you retrieve from the API. This applies to all tools except for the bulk download functions. Filters allow you to specify subsets of data, making it easy to focus on the information that is most relevant to your needs. @@ -573,12 +576,11 @@ crs_filters = get_available_filters(source="crs") multisystem_filters = get_available_filters(source="multisystem") ``` - ## Rate limiting ODA Reader limits outgoing requests to avoid hitting the OECD API too often. Network calls pause automatically when the limit (20 calls per minute by default) -is reached. The limit can be changed via the ``API_RATE_LIMITER`` object: +is reached. The limit can be changed via the `API_RATE_LIMITER` object: ```python from oda_reader import API_RATE_LIMITER @@ -602,3 +604,51 @@ If you have an idea for a new feature, additional functionality, or if you have To contribute code, you can fork the repository, implement your changes, and then open a pull request (PR). Please ensure that you submit an issue beforehand to discuss your proposed changes. Your contributions are invaluable in making ODA Reader better for everyone. + +### Development setup + +This project uses [uv](https://docs.astral.sh/uv/) and follows the +[`bblocks-projects`](https://github.com/ONEcampaign/bblocks-projects) standard +(linting with [ruff](https://docs.astral.sh/ruff/), type checking with +[ty](https://github.com/astral-sh/ty), and pre-commit hooks). + +```bash +uv sync --all-groups # install runtime + dev/test/docs dependencies +uv run pre-commit install # enable the git hooks + +# Run the quality checks locally (these also run in CI): +uv run ruff check . +uv run ruff format --check . +uv run ty check src/oda_reader +uv run pytest -m "not integration" +``` + +### Releasing + +Releases are published to [PyPI](https://pypi.org/project/oda_reader/) +automatically by the `release.yml` GitHub Actions workflow using +[trusted publishing](https://docs.pypi.org/trusted-publishers/) (OIDC — no API +tokens or secrets). The workflow runs only on tags matching `v*`, builds the +sdist and wheel with `uv build`, and publishes from the `pypi` deployment +environment. + +To cut a release: + +1. Bump `version` in `pyproject.toml` and add a dated entry to `CHANGELOG.md`. + +1. Merge those changes to `main`. + +1. Tag the release and push the tag: + + ```bash + git tag v1.2.3 + git push origin v1.2.3 + ``` + +1. The `release.yml` workflow builds and publishes to PyPI. Watch the run in the + repository's **Actions** tab. + +The tag version should match the `version` in `pyproject.toml`. Trusted +publishing is configured on the PyPI project (publisher: `ONEcampaign/oda_reader`, +workflow `release.yml`, environment `pypi`); no maintainer credentials are +needed to publish. diff --git a/docs/docs/advanced.md b/docs/docs/advanced.md index 3dc80cc..1f50ff8 100644 --- a/docs/docs/advanced.md +++ b/docs/docs/advanced.md @@ -26,11 +26,13 @@ print(filter_string) This filter string can be used to manually construct API URLs. **When to use QueryBuilder directly**: + - Building custom SDMX queries - Debugging filter construction - Understanding dimension order for a dataset **Methods available**: + - `build_dac1_filter(donor, sector, measure, flow_type, unit_measure, price_base)` - `build_dac2a_filter(donor, recipient, measure, price_base, ...)` - `build_crs_filter(donor, recipient, sector, channel, modality, microdata, ...)` @@ -47,8 +49,8 @@ OECD occasionally changes dataflow versions (schema updates). ODA Reader handles When a dataflow version returns an error (not found), ODA Reader automatically queries the OECD's SDMX metadata endpoint to discover the latest published version and retries with it: 1. Tries the configured default version -2. If not found, queries the metadata endpoint for the authoritative latest version -3. Retries once with the discovered version +1. If not found, queries the metadata endpoint for the authoritative latest version +1. Retries once with the discovered version This means your code keeps working even when OECD updates schema versions. You'll see a log message indicating which version was discovered. @@ -75,10 +77,12 @@ data = download_dac1( ``` **When to override**: + - You know the correct version for reproducibility - Debugging version-specific issues **Available for**: + - `download_dac1(dataflow_version=...)` - `download_dac2a(dataflow_version=...)` - `download_crs(dataflow_version=...)` @@ -89,16 +93,19 @@ data = download_dac1( OECD uses two SDMX API versions: **API v1** (legacy): + ``` https://sdmx.oecd.org/public/rest/data/OECD.DCD.FSD,DF_DAC1,1.0/... ``` **API v2** (current): + ``` https://sdmx.oecd.org/public/rest/v2/data/dataflow/OECD.DCD.FSD/DF_DAC1/1.0/... ``` ODA Reader uses the appropriate version for each dataset: + - **DAC1, DAC2a**: API v2 - **CRS, Multisystem**: Custom endpoint (CRS-specific API) @@ -167,6 +174,7 @@ combined = pd.merge( ``` **Tips**: + - Use same `pre_process` and `dotstat_codes` settings for compatibility - Column names and codes must align - Filter carefully to avoid double-counting diff --git a/docs/docs/bulk-downloads.md b/docs/docs/bulk-downloads.md index a780714..83bd841 100644 --- a/docs/docs/bulk-downloads.md +++ b/docs/docs/bulk-downloads.md @@ -5,6 +5,7 @@ For large-scale analysis, bulk downloads are faster and more reliable than repea ## When to Use Bulk Downloads **Use bulk downloads when**: + - You need the full CRS dataset (millions of rows) - You're analyzing large year ranges - You want all columns and dimensions @@ -12,6 +13,7 @@ For large-scale analysis, bulk downloads are faster and more reliable than repea - You need reproducible research with exact dataset versions **Use API downloads when**: + - You need filtered subsets (specific donors, recipients, sectors) - Working with smaller datasets (DAC1, DAC2a) - Exploratory analysis with changing queries @@ -90,6 +92,7 @@ for chunk in bulk_download_crs(as_iterator=True): **How it works**: `as_iterator=True` yields one DataFrame per parquet row group (typically 10,000-100,000 rows). You process each chunk sequentially, which keeps memory usage low. **Example use cases**: + - Filtering large files: Process each chunk, save matches - Computing aggregates: Accumulate statistics across chunks - Converting formats: Read parquet chunks, write to CSV/Excel @@ -143,6 +146,7 @@ download_crs_file(year=2022, save_to_path="./data/crs_2022.parquet") ``` **Grouped years**: Older years are grouped in single files: + - Recent years: Individual files (2006-present) - `"2004-05"`: 2004-2005 combined - `"2002-03"`: 2002-2003 combined @@ -206,6 +210,7 @@ download_aiddata(save_to_path="./data/aiddata.parquet") **Critical difference**: Bulk download files from OECD use the **OECD.Stat schema**, not the Data Explorer API schema. This means: + - Column names differ from API downloads - Dimension codes may differ - No `pre_process` or `dotstat_codes` parameters (files are already in .Stat format) @@ -213,16 +218,17 @@ This means: **Example**: API download has columns like: + - `DONOR` → becomes `donor_code` after processing - `RECIPIENT` → becomes `recipient_code` after processing Bulk downloads already have: + - `DonorCode` - `RecipientCode` See [Schema Translation](schema-translation.md) for detailed comparison. - ## Troubleshooting **Out of memory errors**: Use `as_iterator=True` to process in chunks instead of loading the entire file. diff --git a/docs/docs/caching.md b/docs/docs/caching.md index 5d66438..ee0e1e6 100644 --- a/docs/docs/caching.md +++ b/docs/docs/caching.md @@ -7,8 +7,8 @@ ODA Reader uses caching to make repeated queries fast and reduce dependency on O ODA Reader caches three types of data: 1. **HTTP responses**: Raw API responses before processing -2. **DataFrames**: Processed pandas DataFrames after schema translation -3. **Bulk files**: Large parquet/zip files downloaded by `bulk_download_crs`, +1. **DataFrames**: Processed pandas DataFrames after schema translation +1. **Bulk files**: Large parquet/zip files downloaded by `bulk_download_crs`, `download_crs_file`, `bulk_download_dac2a` and `bulk_download_multisystem` All three caches are automatic and transparent - you don't need to change your code to benefit from caching. @@ -31,6 +31,7 @@ print(f"Second call: {time.time() - start:.1f} seconds") ``` **Typical output**: + ``` First call: 15.3 seconds Second call: 0.1 seconds @@ -41,6 +42,7 @@ Cached queries are ~100x faster. ## Cache Location By default, caches are stored in: + ``` src/oda_reader/.cache/ ``` @@ -101,6 +103,7 @@ clear_cache() This removes all cached API responses and DataFrames. Your next query will hit the API again. **When to clear cache**: + - You need the latest data and suspect OECD has updated - Cache has grown too large - You're troubleshooting unexpected results @@ -120,6 +123,7 @@ ODA Reader automatically enforces cache limits across the cache root: - **Max age**: 7 days When you import oda_reader, it checks cache limits: + - Files older than 7 days are deleted - If cache exceeds 2.5 GB, oldest files are deleted first @@ -229,6 +233,7 @@ print(f"HTTP cache: {info['cache_size']} responses cached") ``` **Difference between caches**: + - **HTTP cache**: Raw API responses (before parsing) - **DataFrame cache**: Processed DataFrames (after schema translation) @@ -257,9 +262,9 @@ API_RATE_LIMITER.period = 60 **How rate limiting works**: 1. ODA Reader tracks each API call timestamp -2. Before a new call, it checks if limit is reached -3. If limit reached, it **blocks** (pauses) until period expires -4. Then allows the call to proceed +1. Before a new call, it checks if limit is reached +1. If limit reached, it **blocks** (pauses) until period expires +1. Then allows the call to proceed This is transparent - your code just runs slower when rate limit is reached. diff --git a/docs/docs/changelog.md b/docs/docs/changelog.md index fb14e84..9805a61 100644 --- a/docs/docs/changelog.md +++ b/docs/docs/changelog.md @@ -5,6 +5,7 @@ For a complete version history and release notes, see the [CHANGELOG.md](https:/ ## Recent Releases The changelog includes: + - New features and enhancements - Bug fixes - Breaking changes and migration guides @@ -15,6 +16,7 @@ Visit the [repository changelog](https://github.com/ONEcampaign/oda_reader/blob/ ## Version History ODA Reader follows [semantic versioning](https://semver.org/): + - **Major version** (X.0.0): Breaking changes - **Minor version** (0.X.0): New features, backwards compatible - **Patch version** (0.0.X): Bug fixes, backwards compatible diff --git a/docs/docs/datasets.md b/docs/docs/datasets.md index 9e627d6..a9bd3cc 100644 --- a/docs/docs/datasets.md +++ b/docs/docs/datasets.md @@ -4,13 +4,13 @@ ODA Reader provides access to five datasets covering official development assist ## Quick Reference -| Dataset | What It Contains | Use When | -|---------|------------------|----------| -| **DAC1** | Aggregate flows by donor | Analyzing overall ODA trends, donor performance | -| **DAC2a** | Bilateral flows by donor-recipient | Recipient-level analysis | -| **CRS** | Project-level microdata | Sector analysis, project details, activity-level data | -| **Multisystem** | Multilateral system usage | Analyzing multilateral channels and contributions | -| **AidData** | Chinese development finance | Chinese aid flows | +| Dataset | What It Contains | Use When | +| --------------- | ---------------------------------- | ----------------------------------------------------- | +| **DAC1** | Aggregate flows by donor | Analyzing overall ODA trends, donor performance | +| **DAC2a** | Bilateral flows by donor-recipient | Recipient-level analysis | +| **CRS** | Project-level microdata | Sector analysis, project details, activity-level data | +| **Multisystem** | Multilateral system usage | Analyzing multilateral channels and contributions | +| **AidData** | Chinese development finance | Chinese aid flows | ## DAC1: Aggregate Flows diff --git a/docs/docs/filtering.md b/docs/docs/filtering.md index 2f7cce9..0ec98a1 100644 --- a/docs/docs/filtering.md +++ b/docs/docs/filtering.md @@ -66,6 +66,7 @@ dac1_filters = get_available_filters("dac1") ``` **Output:** + ``` OrderedDict([('donor', typing.Union[str, list, NoneType]), ('sector', typing.Union[str, list, NoneType]), @@ -104,7 +105,7 @@ Common dimensions: - `donor` - Donor country (ISO3 codes like "USA", "GBR", "FRA") - `recipient` - Recipient country or region (DAC2a only) -- `sector` - Sector code (DAC1 only, use "_Z" for not applicable) +- `sector` - Sector code (DAC1 only, use "\_Z" for not applicable) - `measure` - Type of flow (ODA, OOF, grants, loans, etc.) - `flow_type` - Commitments, disbursements, net flows, etc. - `price_base` - "V" for current prices, "Q" for constant prices @@ -156,8 +157,8 @@ education_projects = download_crs( The online OECD Data Explorer shows semi-aggregated CRS data, not microdata. To match that view: 1. Set `microdata: False` -2. Specify `channel: "_T"` (total across channels) -3. Specify `modality: "_T"` (total across modalities) +1. Specify `channel: "_T"` (total across channels) +1. Specify `modality: "_T"` (total across modalities) **Example**: Get semi-aggregated data matching Data Explorer: @@ -216,7 +217,7 @@ print(data['measure'].unique()) # See all measure codes 2. **Check OECD documentation**: Code lists are in the [OECD DAC Glossary](https://www.oecd.org/dac/financing-sustainable-development/development-finance-standards/) -3. **Use trial and error**: Download a small query and examine column values +1. **Use trial and error**: Download a small query and examine column values **Note**: Codes differ between API schema and .Stat schema. When making API calls, you must use the API schema. However by default, ODA Reader returns .Stat codes. See [Schema Translation](schema-translation.md) for details. diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md index dcb8901..640c8b3 100644 --- a/docs/docs/getting-started.md +++ b/docs/docs/getting-started.md @@ -33,6 +33,7 @@ print(data.head()) ``` **Output:** + ``` Downloaded 40119 rows donor_code donor_name aidtype_code aid_type flows_code fund_flows amounttype_code amount_type sector_code sector_name year value base_period unit_multiplier @@ -66,6 +67,7 @@ print(data['donor_name'].unique()) ``` **Output:** + ``` Downloaded 1851 rows for USA and GBR ['United States', 'United Kingdom'] @@ -92,6 +94,7 @@ print(f"Recipients: {sorted(data['recipient_name'].unique())}") ``` **Output:** + ``` Downloaded 3724 rows Recipients: ['Kenya', 'Nigeria'] @@ -104,9 +107,9 @@ DAC2a includes recipient countries as a dimension, making it ideal for analyzing When you ran these examples: 1. **ODA Reader constructed SDMX API queries** - You didn't need to know the complex API syntax -2. **Results were cached** - Run the same query again and it's instant -3. **Rate limiting was applied** - Automatic pauses prevent you from hitting API limits -4. **Schema translation happened** - Codes were converted to .Stat format for compatibility +1. **Results were cached** - Run the same query again and it's instant +1. **Rate limiting was applied** - Automatic pauses prevent you from hitting API limits +1. **Schema translation happened** - Codes were converted to .Stat format for compatibility ## Next Steps diff --git a/docs/docs/schema-translation.md b/docs/docs/schema-translation.md index 6d1c6a3..363a812 100644 --- a/docs/docs/schema-translation.md +++ b/docs/docs/schema-translation.md @@ -29,6 +29,7 @@ ODA Reader provides two parameters to control schema handling: ### `pre_process` (default: `True`) Performs basic cleaning: + - Renames columns to machine-readable names (e.g., `DONOR` → `donor_code`) - Sets proper data types (`int`, `float`, `string`) - Removes empty columns @@ -70,6 +71,7 @@ print(data_clean.columns) ### `dotstat_codes` (default: `True`) Translates dimension **codes** from API format to .Stat format: + - Requires `pre_process=True` to work - Converts codes like donor IDs, measure types, flow codes - Makes data compatible with .Stat bulk downloads and historical data @@ -117,12 +119,14 @@ data = download_dac1(start_year=2022, end_year=2022) ``` **Result**: + - Clean column names: `donor`, `recipient`, `measure`, etc. - .Stat codes: `'USA'`, `'GBR'` for donors - Proper data types set - **Use when**: General analysis, compatibility with historical .Stat data **Pros**: + - Works with existing .Stat-based workflows - Compatible with bulk download files @@ -138,16 +142,19 @@ data = download_dac1( ``` **Result**: + - Raw API column names: `DONOR`, `MEASURE` (all caps) - Raw API codes: numeric or internal codes - No type conversion - **Use when**: Debugging API issues, understanding API structure **Pros**: + - See exactly what OECD API returns - Useful for troubleshooting **Cons**: + - Harder to work with (inconsistent naming) ### Mode 3: Preprocessed with API Codes @@ -162,16 +169,19 @@ data = download_dac1( ``` **Result**: + - Clean column names: `donor`, `recipient`, etc. - API codes: numeric or internal (not .Stat codes) - Proper data types - **Use when**: Working exclusively with new API data, don't need .Stat compatibility **Pros**: + - Clean DataFrame structure - Uses OECD's latest code conventions **Cons**: + - Codes differ from .Stat bulk files - May not match historical datasets @@ -179,24 +189,23 @@ data = download_dac1( ### Donor Codes -| .Stat Code | API code | Country | -|----------|----------|---------| -| `1` | `AUS` | Australia | -| `2` | `AUT` | Austria | -| `12` | `USA` | United States | -| `301` | `GBR` | United Kingdom | +| .Stat Code | API code | Country | +| ---------- | -------- | -------------- | +| `1` | `AUS` | Australia | +| `2` | `AUT` | Austria | +| `12` | `USA` | United States | +| `301` | `GBR` | United Kingdom | ### Measure Codes (DAC1) -| .Stat Code | API Code | Description | -|------------|------------|-------------| -| `100` | `1010` | Net ODA | -| `106` | `1011` | ODA Grants | -| `11017` | `11017` | Grant equiv. of loans | +| .Stat Code | API Code | Description | +| ---------- | -------- | --------------------- | +| `100` | `1010` | Net ODA | +| `106` | `1011` | ODA Grants | +| `11017` | `11017` | Grant equiv. of loans | (Note: Some codes are the same across schemas) - Translation mappings are maintained in `src/oda_reader/schemas/mappings/` as JSON files. ## Bulk Download Schema @@ -218,7 +227,7 @@ There are no `pre_process` or `dotstat_codes` parameters for bulk downloads - th **Combining API and bulk downloads**: -If you mix API (with `.Stat codes) and bulk downloads, they should be compatible. But column **names** may differ slightly: +If you mix API (with \`.Stat codes) and bulk downloads, they should be compatible. But column **names** may differ slightly: ```python # API download with .Stat codes @@ -256,23 +265,26 @@ bulk_data = bulk_data.rename(columns={ **Use default mode (pre_process=True, dotstat_codes=True)**: -- General analysis and research -- Combining API downloads with bulk files -- Working with historical .Stat exports -- Human-readable codes (ISO3 country codes) +- General analysis and research +- Combining API downloads with bulk files +- Working with historical .Stat exports +- Human-readable codes (ISO3 country codes) **Use raw mode (pre_process=False, dotstat_codes=False)**: -- Debugging API issues + +- Debugging API issues - Understanding API response structure **Use API codes mode (pre_process=True, dotstat_codes=False)**: -- Working exclusively with new Data Explorer API -- When you prefer OECD's latest code conventions -- Avoid if combining with bulk downloads or .Stat files + +- Working exclusively with new Data Explorer API +- When you prefer OECD's latest code conventions +- Avoid if combining with bulk downloads or .Stat files ## Finding Code Mappings Code translation mappings are defined in: + ``` src/oda_reader/schemas/mappings/ ├── dac1_mapping.json @@ -305,16 +317,19 @@ You can inspect these files to understand how specific codes translate. ## Troubleshooting **Codes don't match between downloads**: + - Check if one is API download and other is bulk download - Verify `dotstat_codes=True` for API downloads when combining with bulk - Column names differ even with same codes - rename if needed **"Translation failed" errors**: + - Ensure `pre_process=True` when using `dotstat_codes=True` - Some newer API codes may not have .Stat mappings yet - File an issue if you encounter unmapped codes **Unexpected column names**: + - Check `pre_process` setting - raw API uses all-caps names - Bulk downloads have their own naming (can't be changed) diff --git a/docs/docs/why-oda-reader.md b/docs/docs/why-oda-reader.md index 81b6e1e..aa013a3 100644 --- a/docs/docs/why-oda-reader.md +++ b/docs/docs/why-oda-reader.md @@ -1,6 +1,5 @@ # Why ODA Reader? - ## The Problem with OECD DAC Data Access The OECD Development Assistance Committee publishes comprehensive data on official development assistance, but accessing it programmatically is unnecessarily difficult: @@ -20,6 +19,7 @@ The OECD Development Assistance Committee publishes comprehensive data on offici **Approach**: Construct HTTP requests to OECD's SDMX endpoints manually. **Challenges**: + - No Python library means writing your own HTTP client code - Complex URL construction: `https://sdmx.oecd.org/public/rest/v2/data/dataflow/OECD.DCD.FSD/DF_CRS/1.0/USA.NGA....11220?startPeriod=2020&endPeriod=2022` - Manual rate limiting required or risk getting blocked @@ -33,6 +33,7 @@ The OECD Development Assistance Committee publishes comprehensive data on offici **Approach**: Download Parquet, CSV or Excel files from the data-explorer manually. **Challenges**: + - No automation - manual clicking and downloading - Portal URLs change, bookmarks break - File format inconsistencies between download dates @@ -46,6 +47,7 @@ The OECD Development Assistance Committee publishes comprehensive data on offici **Approach**: Use general-purpose SDMX Python libraries like `pandasdmx`. **Challenges**: + - Generic libraries don't handle DAC-specific quirks - No built-in knowledge of which datasets exist or their schemas - Schema translation between API and .Stat formats still manual @@ -59,11 +61,13 @@ The OECD Development Assistance Committee publishes comprehensive data on offici ### Why Both API and Bulk Downloads? **API downloads** are ideal for: + - Filtered queries (specific donors, recipients, years) - Exploratory analysis - Smaller datasets (DAC1, DAC2a) **Bulk downloads** are ideal for: + - Full CRS dataset (millions of rows) - Avoiding slow API calls and rate limits - Reproducible research requiring exact dataset versions @@ -73,6 +77,7 @@ ODA Reader provides both because different analysis workflows need different app ### Why Automatic Caching? API calls to OECD are slow (often 10-30 seconds per query) and subject to rate limiting. Caching means: + - Repeated queries are instant - Less dependency on OECD's server reliability - Iterative analysis doesn't hit rate limits @@ -80,10 +85,8 @@ API calls to OECD are slow (often 10-30 seconds per query) and subject to rate l You can disable or clear caching when you need fresh data. - ## Limitations and When Not to Use ODA Reader - **Requires Python knowledge**: This is a Python package. If you're not comfortable with Python and pandas, the OECD.Stat portal's Excel downloads might be easier. **Mostly focused on DAC data**: ODA Reader focuses on Development Assistance Committee datasets. However, we recently introduced data from Aid Data. diff --git a/pyproject.toml b/pyproject.toml index f275846..c2d44a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,15 +7,13 @@ license = "MIT" authors = [ { name = "Jorge Rivera", email = "jorge.rivera@one.org" } ] -requires-python = ">=3.10" +requires-python = ">=3.11" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: Science/Research", - "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -37,7 +35,7 @@ dependencies = [ [build-system] -requires = ["uv_build>=0.8.24,<0.9.0"] +requires = ["uv_build>=0.11.8,<0.12"] build-backend = "uv_build" [dependency-groups] @@ -46,6 +44,7 @@ dev = [ "pytest>=9.0.3", "pytest-mock>=3.15.1", "ruff>=0.14.0", + "ty>=0.0.33", ] docs = [ "mkdocs>=1.5.0", @@ -71,8 +70,11 @@ constraint-dependencies = [ # Set the maximum line length line-length = 88 -# Target Python 3.10+ -target-version = "py310" +# Target Python 3.11+ +target-version = "py311" + +# Lint and format Jupyter notebooks too +extend-include = ["*.ipynb"] # Exclude common directories exclude = [ @@ -102,13 +104,20 @@ select = [ "SIM", # flake8-simplify "PL", # pylint "RUF", # ruff-specific rules + "ERA", # eradicate (commented-out code) + "PERF", # perflint + "ANN", # flake8-annotations (require type hints on public APIs) ] # Ignore specific rules that might be too strict ignore = [ "E501", # Line too long (handled by formatter) "PLR0913", # Too many arguments + "PLR0912", # Too many branches + "PLR0914", # Too many locals + "PLR0915", # Too many statements "PLR2004", # Magic value used in comparison + "ANN401", # Allow `Any` in typed signatures "RUF022", # Unsorted __all__ (intentional grouping by category) "RUF013", # Implicit Optional (cleaner syntax for defaults) ] @@ -117,10 +126,15 @@ ignore = [ fixable = ["ALL"] unfixable = [] +[tool.ruff.lint.pydocstyle] +convention = "google" + [tool.ruff.lint.per-file-ignores] -# Allow longer lines in tests and examples -"tests/**/*.py" = ["E501"] +# Tests don't need annotations, pylint-magic-value gates, or line-length limits. +"tests/**/*.py" = ["ANN", "PLR2004", "E501"] "docs/examples/**/*.py" = ["E501"] +# Notebooks: exploration style — don't require annotations; allow REPL patterns. +"**/*.ipynb" = ["ANN", "F401", "F811", "E402"] # Allow global variable usage in singleton-managing modules (architectural decision) "src/oda_reader/_cache/*.py" = ["PLW0603"] "src/oda_reader/common.py" = ["PLW0603"] @@ -160,3 +174,49 @@ addopts = [ "--strict-markers", "-m", "not integration", ] + +[tool.ty.environment] +python-version = "3.11" + +[tool.ty.src] +include = ["src/oda_reader"] + +[tool.ty.analysis] +# Replace pandas / numpy type information with `Any`. Eliminates the cascade of +# false positives from those libraries' incomplete or absent stubs without +# disabling type checking on our own code. oda_reader is pandas-heavy, so this +# keeps the remaining ty diagnostics genuine. +# https://docs.astral.sh/ty/reference/configuration/ +replace-imports-with-any = [ + "pandas", + "pandas.**", + "numpy", + "numpy.**", +] + +[tool.ty.rules] +# These warn-by-default rules fire frequently on dynamic dataframe / dict +# patterns. Demoted to ignore. +possibly-missing-attribute = "ignore" +possibly-unresolved-reference = "ignore" + +[tool.coverage.run] +source = ["src"] +branch = true +omit = [ + "*/tests/*", + "*/__pycache__/*", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "def __str__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] +show_missing = true +precision = 2 diff --git a/src/oda_reader/_cache/README.md b/src/oda_reader/_cache/README.md index 7b14df0..a58e774 100644 --- a/src/oda_reader/_cache/README.md +++ b/src/oda_reader/_cache/README.md @@ -2,37 +2,38 @@ This document provides a comprehensive guide to the caching system in `oda_reader`, including architecture details, usage instructions, and backward compatibility notes. ---- +______________________________________________________________________ ## Table of Contents 1. [Overview](#overview) -2. [Architecture](#architecture) -3. [Quick Start](#quick-start) -4. [Configuration](#configuration) -5. [Cache Management](#cache-management) -6. [Backward Compatibility](#backward-compatibility) -7. [Performance Considerations](#performance-considerations) -8. [Troubleshooting](#troubleshooting) -9. [Technical Details](#technical-details) +1. [Architecture](#architecture) +1. [Quick Start](#quick-start) +1. [Configuration](#configuration) +1. [Cache Management](#cache-management) +1. [Backward Compatibility](#backward-compatibility) +1. [Performance Considerations](#performance-considerations) +1. [Troubleshooting](#troubleshooting) +1. [Technical Details](#technical-details) ---- +______________________________________________________________________ ## Overview The `oda_reader` package uses a **three-tier caching system** to optimize data downloads from the OECD API: 1. **HTTP Cache** (requests-cache): Caches raw API responses for 7 days -2. **DataFrame Cache**: Caches processed DataFrames with preprocessing parameters -3. **Bulk File Cache**: Caches large bulk downloads (CRS, Multisystem, AidData) +1. **DataFrame Cache**: Caches processed DataFrames with preprocessing parameters +1. **Bulk File Cache**: Caches large bulk downloads (CRS, Multisystem, AidData) This multi-layer approach provides: + - **Fast repeated queries** (10-90x speedup on cache hits) - **Correct data** (cache keys include all processing parameters) - **Efficient storage** (parquet format, automatic cleanup) - **Platform-aware paths** (follows OS conventions) ---- +______________________________________________________________________ ## Architecture @@ -79,7 +80,7 @@ Default location: `~/.cache/oda-reader/{version}/` (macOS/Linux) or `%LOCALAPPDA **Note**: Cache is automatically versioned - upgrading `oda_reader` creates a new cache directory, ensuring compatibility. ---- +______________________________________________________________________ ## Quick Start @@ -115,7 +116,7 @@ print(oda_reader.bulk_cache_manager().stats()) # {'total_entries': 1, 'total_size_mb': 878.5, 'stale_entries': 0} ``` ---- +______________________________________________________________________ ## Configuration @@ -144,9 +145,10 @@ export ODA_READER_CACHE_DIR="/custom/cache/path" ``` Priority order: + 1. `set_cache_dir()` (programmatic override) -2. `ODA_READER_CACHE_DIR` (environment variable) -3. Platform default (via platformdirs) +1. `ODA_READER_CACHE_DIR` (environment variable) +1. Platform default (via platformdirs) ### Disable Caching @@ -180,7 +182,7 @@ oda_reader.API_RATE_LIMITER.max_calls = 10 oda_reader.API_RATE_LIMITER.period = 60 ``` ---- +______________________________________________________________________ ## Cache Management @@ -231,7 +233,7 @@ oda_reader.enforce_cache_limits( **Note**: This is called automatically on first cache access, not at import time. ---- +______________________________________________________________________ ## Backward Compatibility @@ -255,6 +257,7 @@ oda_reader.enforce_cache_limits() # Enforces size/age limits If you have code using the old caching system: **Before** (oda_reader < 1.2.2): + ```python from oda_reader._cache import memory, set_cache_dir @@ -267,6 +270,7 @@ set_cache_dir("/custom/path") ``` **After** (oda_reader >= 1.2.2): + ```python from oda_reader import get_cache_dir, set_cache_dir @@ -276,6 +280,7 @@ set_cache_dir("/custom/path") ``` **Key Changes**: + - ✅ No breaking changes - old code continues to work - ✅ Cache location moved from `src/oda_reader/.cache/` to platform directory - ✅ joblib replaced with requests-cache + parquet files @@ -287,6 +292,7 @@ set_cache_dir("/custom/path") The old caching system had several issues: 1. **Data correctness bug**: Cache didn't include `pre_process` or `dotstat_codes` parameters + ```python # Before: These returned the SAME cached data (wrong!) df1 = download_dac1(2022, 2022, pre_process=True, dotstat_codes=True) @@ -295,25 +301,27 @@ The old caching system had several issues: # After: These correctly return different data ``` -2. **Bad cache location**: Cache was in `src/oda_reader/.cache/` (polluted source tree) +1. **Bad cache location**: Cache was in `src/oda_reader/.cache/` (polluted source tree) -3. **Import-time slowdown**: `enforce_cache_limits()` walked entire cache on every import +1. **Import-time slowdown**: `enforce_cache_limits()` walked entire cache on every import -4. **No observability**: No way to inspect cache contents or hit/miss rates +1. **No observability**: No way to inspect cache contents or hit/miss rates All these issues are now fixed while maintaining full backward compatibility. ---- +______________________________________________________________________ ## Performance Considerations ### Cache Hit Performance Typical speedups with cache hits: + - **HTTP cache hit**: ~2-5x faster (avoids network request) - **DataFrame cache hit**: ~10-90x faster (avoids parsing + processing) Example benchmark: + ``` First download: 2.71s (API + processing) Second download: 0.03s (DataFrame cache) - 90x faster @@ -322,6 +330,7 @@ Second download: 0.03s (DataFrame cache) - 90x faster ### Storage Usage Typical cache sizes: + - HTTP cache: 1-10 MB per response (filesystem backend, can handle >2GB responses) - DataFrame cache: 0.1-1 MB per query (compressed parquet) - Bulk files: 100-1000 MB per file (CRS full dataset ~900 MB) @@ -335,17 +344,19 @@ Typical cache sizes: ### When Cache Is NOT Used Cache is bypassed when: + 1. Caching is disabled (`disable_cache()` or `disable_http_cache()`) -2. Cache entry has expired (HTTP: 7 days, bulk files: per-entry TTL) -3. Different parameters are used (cache keys are unique per parameter combination) +1. Cache entry has expired (HTTP: 7 days, bulk files: per-entry TTL) +1. Different parameters are used (cache keys are unique per parameter combination) ---- +______________________________________________________________________ ## Troubleshooting ### Cache Not Working **Check if caching is enabled:** + ```python import oda_reader @@ -355,6 +366,7 @@ print(oda_reader.dataframe_cache().stats()) ``` **Common issues:** + - Caching disabled: Call `oda_reader.enable_cache()` - Cache full: Call `oda_reader.clear_cache()` or increase limits - Different parameters: Cache keys are unique per parameter combination @@ -362,6 +374,7 @@ print(oda_reader.dataframe_cache().stats()) ### Cache Growing Too Large **Check cache size:** + ```python import oda_reader @@ -375,6 +388,7 @@ print(oda_reader.bulk_cache_manager().stats()) ``` **Solutions:** + ```python # Clear specific caches oda_reader.dataframe_cache().clear() # Usually the culprit @@ -390,6 +404,7 @@ oda_reader.enforce_cache_limits(max_size_mb=1000) # 1 GB limit ### Cache Returning Stale Data **Force fresh data:** + ```python # Option 1: Clear cache before download oda_reader.clear_http_cache() @@ -423,7 +438,7 @@ manager = CacheManager() manager._lock = FileLock(manager.lock_path, timeout=2000) ``` ---- +______________________________________________________________________ ## Technical Details @@ -441,6 +456,7 @@ oda_reader/_cache/ ### Cache Key Generation **DataFrame cache keys** are SHA256 hashes of: + ```python { "dataflow_id": "DSD_DAC1@DF_DAC1", @@ -459,6 +475,7 @@ This ensures different preprocessing options get separate cache entries. ### HTTP Cache Backend Uses `requests-cache` with filesystem backend: + - Directory: `{cache_dir}/http_cache/` - Stores responses, redirects, and metadata as individual files - Handles large responses (>2GB) without issues @@ -475,6 +492,7 @@ Uses `requests-cache` with filesystem backend: ### Bulk File Cache Follows pydeflate design: + - Manifest: `{cache_dir}/bulk_files/manifest.json` - Lock file: `{cache_dir}/bulk_files/.cache.lock` (FileLock) - Atomic writes: temp-file-then-rename pattern @@ -483,17 +501,19 @@ Follows pydeflate design: ### Version-Based Cache Invalidation Cache directory includes package version: + ``` ~/.cache/oda-reader/1.2.2/ # Version 1.2.2 ~/.cache/oda-reader/1.3.0/ # Version 1.3.0 (new cache) ``` This ensures: + - No cache corruption after upgrades - Schema changes don't break existing cache - Automatic cleanup of old versions ---- +______________________________________________________________________ ## API Reference @@ -533,4 +553,4 @@ This ensures: - `clear_cache() -> None`: Clear entire cache directory - `enforce_cache_limits() -> None`: Enforce size/age limits ---- +______________________________________________________________________ diff --git a/src/oda_reader/_cache/dataframe.py b/src/oda_reader/_cache/dataframe.py index a3d2b10..9756b46 100644 --- a/src/oda_reader/_cache/dataframe.py +++ b/src/oda_reader/_cache/dataframe.py @@ -36,7 +36,7 @@ def _make_cache_key( url: str, pre_process: bool, dotstat_codes: bool, - **kwargs, + **kwargs: Any, ) -> str: """Generate a deterministic cache key for a DataFrame query. @@ -82,7 +82,7 @@ class DataFrameCache: Uses parquet files for efficient storage and fast loading. """ - def __init__(self, cache_dir: Path | None = None): + def __init__(self, cache_dir: Path | None = None) -> None: """Initialize DataFrame cache. Args: @@ -99,7 +99,7 @@ def get( url: str, pre_process: bool, dotstat_codes: bool, - **kwargs, + **kwargs: Any, ) -> pd.DataFrame | None: """Get a cached DataFrame if it exists. @@ -148,7 +148,7 @@ def set( url: str, pre_process: bool, dotstat_codes: bool, - **kwargs, + **kwargs: Any, ) -> None: """Cache a processed DataFrame. diff --git a/src/oda_reader/_cache/legacy.py b/src/oda_reader/_cache/legacy.py index e402c72..10f2a46 100644 --- a/src/oda_reader/_cache/legacy.py +++ b/src/oda_reader/_cache/legacy.py @@ -11,7 +11,9 @@ import os import shutil import time +from collections.abc import Callable from pathlib import Path +from typing import Any from oda_reader._cache.config import get_cache_dir from oda_reader._cache.config import set_cache_dir as _impl_set_cache_dir @@ -29,7 +31,7 @@ _JOBLIB_MEMORY: object | None = None -def memory(): +def memory() -> object: """Return a dummy memory store (deprecated). This function is kept for backward compatibility but no longer uses joblib. @@ -56,7 +58,7 @@ def cache_dir() -> Path: return get_cache_dir() -def set_cache_dir(path) -> None: +def set_cache_dir(path: str | Path) -> None: """Set a custom cache directory path (deprecated). Use oda_reader._cache.config.set_cache_dir() instead. @@ -175,13 +177,13 @@ def enforce_cache_limits( ) -def cache_info(func): +def cache_info(func: Callable[..., Any]) -> Callable[..., Any]: """Decorator that logs cache info (deprecated). This decorator is kept for backward compatibility with existing code. """ - def wrapper(*args, **kwargs): + def wrapper(*args: Any, **kwargs: Any) -> Any: global _has_logged_cache_message if not _has_logged_cache_message: logger.info("[oda-reader] Caching is enabled.") diff --git a/src/oda_reader/_cache/manager.py b/src/oda_reader/_cache/manager.py index aaca18b..56a43c8 100644 --- a/src/oda_reader/_cache/manager.py +++ b/src/oda_reader/_cache/manager.py @@ -9,7 +9,7 @@ import os from collections.abc import Callable from dataclasses import dataclass -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from pathlib import Path from typing import Any @@ -137,7 +137,7 @@ def ensure(self, entry: CacheEntry, refresh: bool = False) -> Path: manifest[entry.key] = { "filename": entry.filename, - "downloaded_at": datetime.now(timezone.utc).strftime(ISO_FORMAT), + "downloaded_at": datetime.now(UTC).strftime(ISO_FORMAT), "ttl_days": entry.ttl_days, "version": entry.version, } @@ -187,7 +187,7 @@ def list_records(self) -> list[dict[str, Any]]: size_mb = file_path.stat().st_size / (1024 * 1024) downloaded = datetime.strptime(record["downloaded_at"], ISO_FORMAT) - age = datetime.now(timezone.utc) - downloaded + age = datetime.now(UTC) - downloaded records.append( { @@ -240,7 +240,7 @@ def _is_stale(self, record: dict, entry: CacheEntry) -> bool: return True downloaded = datetime.strptime(record["downloaded_at"], ISO_FORMAT) - age = datetime.now(timezone.utc) - downloaded + age = datetime.now(UTC) - downloaded ttl = timedelta(days=entry.ttl_days) return age > ttl @@ -269,7 +269,7 @@ def _save_manifest(self, manifest: dict) -> None: def _sweep_stale_tmp_files(self) -> None: """Remove *.tmp-* files in base_dir that are older than 24 hours.""" - now = datetime.now(timezone.utc).timestamp() + now = datetime.now(UTC).timestamp() for tmp_file in self.base_dir.glob("*.tmp-*"): try: age = now - tmp_file.stat().st_mtime diff --git a/src/oda_reader/_http_primitives.py b/src/oda_reader/_http_primitives.py index e2bca73..6448195 100644 --- a/src/oda_reader/_http_primitives.py +++ b/src/oda_reader/_http_primitives.py @@ -11,6 +11,7 @@ import logging import time from collections import deque +from typing import cast import requests import requests_cache @@ -57,7 +58,7 @@ def wait(self) -> None: # Global HTTP cache session (initialized lazily) _HTTP_SESSION: requests_cache.CachedSession | None = None -_CACHE_ENABLED = True +_CACHE_ENABLED: bool = True def _get_http_session() -> requests_cache.CachedSession: @@ -112,7 +113,7 @@ def get_response_text(url: str, headers: dict) -> tuple[int, str, bool]: from_cache = False logger.info(f"Fetching data from API (cache disabled): {url}") - return response.status_code, response.text, from_cache + return cast(int, response.status_code), response.text, from_cache def get_response_content(url: str, headers: dict) -> tuple[int, bytes, bool]: @@ -142,4 +143,4 @@ def get_response_content(url: str, headers: dict) -> tuple[int, bytes, bool]: from_cache = False logger.info(f"Fetching data from API (cache disabled): {url}") - return response.status_code, response.content, from_cache + return cast(int, response.status_code), response.content, from_cache diff --git a/src/oda_reader/aiddata.py b/src/oda_reader/aiddata.py index 394e835..5382128 100644 --- a/src/oda_reader/aiddata.py +++ b/src/oda_reader/aiddata.py @@ -15,8 +15,8 @@ def filter_years( df: pd.DataFrame, - start_year: int = None, - end_year: int = None, + start_year: int | None = None, + end_year: int | None = None, ) -> pd.DataFrame: """Filters a dataframe by year range. If the provided time range is not a subset of the available years, it returns the entire dataframe. diff --git a/src/oda_reader/common.py b/src/oda_reader/common.py index 0ec67e8..8e5e031 100644 --- a/src/oda_reader/common.py +++ b/src/oda_reader/common.py @@ -203,11 +203,11 @@ def _extract_dataflow_id(url: str) -> str | None: The dataflow identifier (e.g. ``"DSD_DAC1@DF_DAC1"``) if found, ``None`` if the URL does not match a recognised pattern. """ - # v1: AGENCY,DATAFLOW_ID,VERSION + # v1 pattern: AGENCY,DATAFLOW_ID,VERSION (comma-separated) match = re.search(r"OECD\.DCD\.FSD,([^,/]+),\d+\.\d+", url) if match: return match.group(1) - # v2: AGENCY/DATAFLOW_ID/VERSION + # v2 pattern: AGENCY/DATAFLOW_ID/VERSION (slash-separated) match = re.search(r"OECD\.DCD\.FSD/([^/]+)/\d+\.\d+", url) return match.group(1) if match else None diff --git a/src/oda_reader/crs.py b/src/oda_reader/crs.py index b1386a4..0cc7986 100644 --- a/src/oda_reader/crs.py +++ b/src/oda_reader/crs.py @@ -17,21 +17,21 @@ # Default version; actual version is discovered dynamically on fallback DATAFLOW_VERSION: str = "1.6" -# CRS filter structure: -# {donor}.{recipient}.{sector}.{measure}.{channel}. -# {modality}.{flow_type}.{price_base}.{md_dim}.{md_id}.{unit_measure}. -# {time_period} +# CRS filter structure (dimension order): +# donor, recipient, sector, measure, channel, +# modality, flow_type, price_base, md_dim, md_id, unit_measure, +# time_period -def get_full_crs_parquet_id(): +def get_full_crs_parquet_id() -> str: return get_bulk_file_id(flow_url=CRS_FLOW_URL, search_string="CRS-Parquet") -def get_reduced_crs_parquet_id(): +def get_reduced_crs_parquet_id() -> str: return get_bulk_file_id(flow_url=CRS_FLOW_URL, search_string="CRS-reduced-parquet") -def get_year_crs_zip_id(year: int): +def get_year_crs_zip_id(year: int | str) -> str: return get_bulk_file_id( flow_url=CRS_FLOW_URL, search_string=f"CRS {year} (dotStat format)" ) diff --git a/src/oda_reader/download/_deflate64.py b/src/oda_reader/download/_deflate64.py index 33021bb..5c65967 100644 --- a/src/oda_reader/download/_deflate64.py +++ b/src/oda_reader/download/_deflate64.py @@ -17,8 +17,8 @@ import inflate64 _DEFLATE64 = 9 -_original_check = zipfile._check_compression -_original_decompressor = zipfile._get_decompressor +_original_check = zipfile._check_compression # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] +_original_decompressor = zipfile._get_decompressor # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] class _Deflate64Decompressor: @@ -26,28 +26,28 @@ class _Deflate64Decompressor: expected by ``zipfile.ZipExtFile``: a ``decompress(data)`` method and an ``eof`` attribute.""" - def __init__(self): + def __init__(self) -> None: self._inflater = inflate64.Inflater() - def decompress(self, data): + def decompress(self, data: bytes) -> bytes: return self._inflater.inflate(data) @property - def eof(self): + def eof(self) -> bool: return self._inflater.eof -def _check_compression_with_deflate64(compression): +def _check_compression_with_deflate64(compression: int) -> None: if compression == _DEFLATE64: return return _original_check(compression) -def _get_decompressor_with_deflate64(compress_type): +def _get_decompressor_with_deflate64(compress_type: int) -> object: if compress_type == _DEFLATE64: return _Deflate64Decompressor() return _original_decompressor(compress_type) -zipfile._check_compression = _check_compression_with_deflate64 -zipfile._get_decompressor = _get_decompressor_with_deflate64 +zipfile._check_compression = _check_compression_with_deflate64 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] +zipfile._get_decompressor = _get_decompressor_with_deflate64 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] diff --git a/src/oda_reader/download/download_tools.py b/src/oda_reader/download/download_tools.py index 05b6898..c6043d5 100644 --- a/src/oda_reader/download/download_tools.py +++ b/src/oda_reader/download/download_tools.py @@ -56,7 +56,7 @@ ) -def _detect_delimiter(file_obj, sample_size: int = 8192) -> str: +def _detect_delimiter(file_obj: typing.IO[bytes], sample_size: int = 8192) -> str: """Detect the delimiter used in a CSV/text file. Reads a sample of the file and uses csv.Sniffer to detect the delimiter. diff --git a/src/oda_reader/download/query_builder.py b/src/oda_reader/download/query_builder.py index e314e67..f01340d 100644 --- a/src/oda_reader/download/query_builder.py +++ b/src/oda_reader/download/query_builder.py @@ -65,36 +65,38 @@ def __init__( ) # Initialize the query parameters with the default format - self.params = {"format": FORMAT} + self.params: dict[str, str | int] = {"format": FORMAT} # Store the API version self.api_version = api_version - def _to_filter_str(self, param: str | list[str] | None) -> str: - """Convert a string parameter to a list, if it is not already a list. + def _to_filter_str(self, param: str | int | list[str] | list[int] | None) -> str: + """Convert a parameter to a filter string. Args: - param (str | list[str] | None): The parameter to convert. - api_version (int): The version of the API to use. + param (str | int | list[str] | list[int] | None): The parameter to convert. Returns: - list[str]: The parameter as a list. + str: The parameter as a filter string. """ if param is None: return "*" if self.api_version == 2 else "" - if isinstance(param, str): - param = [param] - if (self.api_version == 2) & (len(param) > 1): + items: list[str | int] = ( + [param] if isinstance(param, (str, int)) else list(param) + ) + str_param = [str(p) for p in items] + + if (self.api_version == 2) & (len(str_param) > 1): logger.info( f"API version 2 does not support filtering on multiple values:" - f"\n{(', '.join(param))} \n" + f"\n{(', '.join(str_param))} \n" "Returning all values." ) return "*" - return "+".join(param) + return "+".join(str_param) def set_time_period( self, start: int | str | None, end: int | str | None @@ -167,7 +169,7 @@ def build_dac2a_filter( self, donor: str | list[str] | None = None, recipient: str | list[str] | None = None, - measure: int | list[int] | None = None, + measure: str | int | list[str] | list[int] | None = None, unit_measure: str | list[str] | None = None, price_base: str | list[str] | None = None, ) -> str: @@ -200,9 +202,9 @@ def build_crs_filter( self, donor: str | list[str] | None = None, recipient: str | list[str] | None = None, - sector: int | list[int] | None = None, - measure: int | list[int] | None = None, - channel: str | list[str] | None = None, + sector: str | int | list[str] | list[int] | None = None, + measure: str | int | list[str] | list[int] | None = None, + channel: str | int | list[str] | list[int] | None = None, modality: str | list[str] | None = None, flow_type: str | list[str] | None = None, price_base: str | list[str] | None = None, @@ -259,9 +261,9 @@ def build_multisystem_filter( self, donor: str | list[str] | None = None, recipient: str | list[str] | None = None, - sector: int | list[int] | None = None, - measure: int | list[int] | None = None, - channel: int | list[int] | None = None, + sector: str | int | list[str] | list[int] | None = None, + measure: str | int | list[str] | list[int] | None = None, + channel: str | int | list[str] | list[int] | None = None, flow_type: str | list[str] | None = None, price_base: str | list[str] | None = None, ) -> str: @@ -336,7 +338,7 @@ def set_last_n_observations(self, n: int) -> "QueryBuilder": self.params["lastNObservations"] = n return self - def set_format(self, file_format) -> "QueryBuilder": + def set_format(self, file_format: str) -> "QueryBuilder": """Set the format of the output file. Args: diff --git a/src/oda_reader/download/version_discovery.py b/src/oda_reader/download/version_discovery.py index 3d538e9..bfb190a 100644 --- a/src/oda_reader/download/version_discovery.py +++ b/src/oda_reader/download/version_discovery.py @@ -119,7 +119,9 @@ def get_dimension_count(dataflow_id: str, version: str) -> int: ValueError: If no dimensions are found in the response. """ # The DSD ID is the part before '@' in the dataflow ID - dsd_id = dataflow_id.split("@")[0] if "@" in dataflow_id else dataflow_id + dsd_id = ( + dataflow_id.split("@", maxsplit=1)[0] if "@" in dataflow_id else dataflow_id + ) url = f"{DSD_BASE_URL}/{dsd_id}/{version}" status_code, text, _ = get_response_text(url, headers={}) diff --git a/src/oda_reader/multisystem.py b/src/oda_reader/multisystem.py index a81444b..3024bb4 100644 --- a/src/oda_reader/multisystem.py +++ b/src/oda_reader/multisystem.py @@ -17,7 +17,7 @@ DATAFLOW_VERSION: str = "1.6" -def get_full_multisystem_id(): +def get_full_multisystem_id() -> str: return get_bulk_file_id( flow_url=MULTI_FLOW_URL, search_string="Entire dataset (dotStat format)" ) diff --git a/src/oda_reader/py.typed b/src/oda_reader/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/oda_reader/schemas/dac1_translation.py b/src/oda_reader/schemas/dac1_translation.py index 02d45ad..77ad70a 100644 --- a/src/oda_reader/schemas/dac1_translation.py +++ b/src/oda_reader/schemas/dac1_translation.py @@ -27,7 +27,7 @@ ) -def update_dac1_translation_mappings(): +def update_dac1_translation_mappings() -> None: """Pipeline to update the DAC1 translation mappings""" xml_data = parse_xml(xml_url=DAC1_TRANSLATION_SCHEMA_URL)["Structures"] diff --git a/src/oda_reader/schemas/dac2_translation.py b/src/oda_reader/schemas/dac2_translation.py index 0c6678b..6e7b021 100644 --- a/src/oda_reader/schemas/dac2_translation.py +++ b/src/oda_reader/schemas/dac2_translation.py @@ -19,7 +19,7 @@ } -def update_dac2_translation_mappings(): +def update_dac2_translation_mappings() -> None: """Pipeline to update the DAC2A translation mappings""" xml_data = parse_xml(xml_url=DAC2_TRANSLATION_SCHEMA_URL)["Structures"] diff --git a/src/oda_reader/schemas/xml_tools.py b/src/oda_reader/schemas/xml_tools.py index f9c677b..6848b54 100644 --- a/src/oda_reader/schemas/xml_tools.py +++ b/src/oda_reader/schemas/xml_tools.py @@ -1,3 +1,4 @@ +import collections.abc import json from pathlib import Path from xml.etree import ElementTree as ET @@ -29,7 +30,7 @@ def download_xml(xml_url: str) -> requests.models.Response: return response -def xml_to_dict(root) -> dict: +def xml_to_dict(root: ET.Element) -> dict: """Convert an XML file to a dictionary. Args: @@ -107,7 +108,7 @@ def keys_to_int(dictionary: dict) -> dict: return {int(k): v for k, v in dictionary.items() if k.isdigit()} -def save_dict_to_json(dictionary: dict, filename: str) -> None: +def save_dict_to_json(dictionary: dict, filename: str | Path) -> None: """Saves a dictionary to a JSON file.""" # Save the mapping to a JSON file with open(rf"{filename}", "w") as f: @@ -129,7 +130,7 @@ def representation_mapping_to_dict(representation_mapping: list) -> dict: } -def representation_to_json(xml_dict, index: int, filename: str) -> None: +def representation_to_json(xml_dict: dict, index: int, filename: str | Path) -> None: """Pipeline to extract and save a representation mapping to a JSON file.""" # Get the codes from the XML dictionary codes = extract_representation_mapping(xml_dict, index=index) @@ -144,28 +145,30 @@ def representation_to_json(xml_dict, index: int, filename: str) -> None: logger.info(f"Saved {filename} to disk.") -def extract_dac_to_area_codes(xml_dict: dict, filename: str) -> None: +def extract_dac_to_area_codes(xml_dict: dict, filename: str | Path) -> None: """Extracts the DAC1 codes to Area codes from the XML file.""" # Convert the representation to a JSON file representation_to_json(xml_dict, index=0, filename=filename) -def extract_datatypes_to_prices_codes(xml_dict: dict, filename: str) -> None: +def extract_datatypes_to_prices_codes(xml_dict: dict, filename: str | Path) -> None: """Extracts the Datatypes to Prices codes from the XML file.""" # Convert the representation to a JSON file representation_to_json(xml_dict, index=1, filename=filename) -def extract_flowtype_to_flowtype_codes(xml_dict: dict, filename: str) -> None: +def extract_flowtype_to_flowtype_codes(xml_dict: dict, filename: str | Path) -> None: """Extracts the Flowtype to Flowtype codes from the XML file.""" # Convert the representation to a JSON file representation_to_json(xml_dict, index=2, filename=filename) -def read_mapping(mapping_path: str, keys_as_int: bool, update: callable) -> dict: +def read_mapping( + mapping_path: str | Path, keys_as_int: bool, update: collections.abc.Callable +) -> dict: # Read the mapping from a JSON file. If it doesn't exist, create it. if not Path(mapping_path).exists(): diff --git a/tests/README.md b/tests/README.md index ff7b700..754c05d 100644 --- a/tests/README.md +++ b/tests/README.md @@ -29,6 +29,7 @@ Pytest configuration is in `pyproject.toml` under `[tool.pytest.ini_options]`. ## Running Tests ### Run all unit tests (default, fast) + ```bash uv run pytest tests/ ``` @@ -36,6 +37,7 @@ uv run pytest tests/ By default, integration tests are excluded to keep test runs fast. Only unit tests with mocked dependencies will run. ### Run all tests including integration + ```bash uv run pytest tests/ -m "" ``` @@ -43,21 +45,25 @@ uv run pytest tests/ -m "" This will run the full suite, including integration tests that make real API calls. ### Run only integration tests + ```bash uv run pytest tests/ -m integration ``` ### Run only unit tests (explicit) + ```bash uv run pytest tests/ -m unit ``` ### Run specific test file + ```bash uv run pytest tests/common/unit/test_rate_limiter.py -v ``` ### Run specific test class or function + ```bash # Run a specific test class uv run pytest tests/common/unit/test_rate_limiter.py::TestRateLimiterBlocking -v @@ -67,6 +73,7 @@ uv run pytest tests/common/unit/test_rate_limiter.py::TestRateLimiterBlocking::t ``` ### Run with coverage + ```bash uv run pytest tests/ --cov=src/oda_reader --cov-report=html ``` @@ -74,6 +81,7 @@ uv run pytest tests/ --cov=src/oda_reader --cov-report=html This generates an HTML coverage report in `htmlcov/index.html`. ### Run in parallel (unit tests only) + ```bash uv run pytest tests/ -n auto -m "not integration" ``` @@ -110,13 +118,15 @@ def test_real_api_call(): ### Unit Tests Unit tests should: + - Mock external dependencies (HTTP calls, file I/O) -- Be fast (<100ms per test) +- Be fast (\<100ms per test) - Test business logic in isolation - Use parametrization for comprehensive coverage - Focus on edge cases and error handling Example: + ```python import pytest @@ -134,6 +144,7 @@ def test_query_builder_filter(mocker): ### Integration Tests Integration tests should: + - Use real API calls (no mocking) - Be marked with `@pytest.mark.integration` - Use small queries (single year, specific filters) to minimize API load @@ -142,6 +153,7 @@ Integration tests should: - Test critical user-facing functionality Example: + ```python import pytest from oda_reader import dac1, enable_http_cache @@ -191,6 +203,7 @@ def test_filter_conversion(input_val, expected): Key fixtures available in all tests (defined in `conftest.py`): ### `temp_cache_dir` + Creates a temporary cache directory for testing cache behavior. ```python @@ -201,6 +214,7 @@ def test_cache_behavior(temp_cache_dir): ``` ### `rate_limiter_fast` + Provides a fast rate limiter for testing (2 calls per 0.5 seconds). ```python @@ -212,6 +226,7 @@ def test_rate_limiting(rate_limiter_fast): ``` ### `sample_csv_response` + Returns sample CSV data for mocking API responses. ```python @@ -221,6 +236,7 @@ def test_csv_parsing(sample_csv_response): ``` ### `fixtures_dir` + Returns the path to the fixtures directory. ```python @@ -239,6 +255,7 @@ The `disable_cache_for_tests` fixture runs automatically for all tests, ensuring Helper functions are available in `tests/utils.py`: ### `assert_dataframe_schema(df, expected_columns)` + Validates DataFrame has expected columns and types. ```python @@ -253,6 +270,7 @@ def test_dataframe_structure(): ``` ### `load_json_fixture(fixtures_dir, fixture_name)` + Loads a JSON fixture file. ```python @@ -264,6 +282,7 @@ def test_with_fixture(fixtures_dir): ``` ### `mock_api_response(status_code, text, from_cache=False)` + Creates a mock API response tuple. ```python @@ -282,11 +301,13 @@ def test_api_error_handling(mocker): Tests run automatically in GitHub Actions: ### On Every Commit + - **Unit tests only** (~1-2 minutes) - Runs on Python 3.10 - 3.13 - Must pass before merge ### On Pull Requests to Main + - **Full suite** including integration tests (~5-10 minutes) - Runs on Python 3.12 - Lint checks with ruff @@ -319,6 +340,7 @@ open htmlcov/index.html ``` Coverage goals: + - Overall: >80% - Core modules (common, download, query_builder): >90% - Dataset modules (dac1, dac2a, crs): >70% @@ -326,47 +348,55 @@ Coverage goals: ## Troubleshooting ### Tests are slow + By default, integration tests are skipped. If tests are still slow: + - Ensure you're running unit tests only: `uv run pytest tests/ -m "not integration"` - Use parallel execution: `uv run pytest tests/ -n auto` ### Integration tests fail with rate limit errors + - Reduce the number of concurrent test runs - Check that `enable_http_cache()` is called in integration tests - Wait a minute between test runs to respect API rate limits ### Import errors + Make sure dependencies are installed: + ```bash uv sync --group test ``` ### Cache-related test failures + The cache is disabled by default in tests. If you need to test cache behavior: + 1. Use the `@pytest.mark.cache` marker -2. Manually enable cache in the test with `enable_http_cache()` -3. Use the `temp_cache_dir` fixture for isolation +1. Manually enable cache in the test with `enable_http_cache()` +1. Use the `temp_cache_dir` fixture for isolation ## Best Practices 1. **Test behavior, not implementation**: Focus on what the code does, not how it does it -2. **Keep tests independent**: Each test should be able to run in isolation -3. **Use descriptive names**: Test names should clearly describe what they test -4. **Arrange-Act-Assert**: Structure tests with clear setup, execution, and verification phases -5. **Don't test the framework**: Trust that pandas, requests, etc. work correctly -6. **Mock at boundaries**: Mock HTTP calls and file I/O, not internal functions -7. **Keep integration tests focused**: Test critical paths only, use small queries +1. **Keep tests independent**: Each test should be able to run in isolation +1. **Use descriptive names**: Test names should clearly describe what they test +1. **Arrange-Act-Assert**: Structure tests with clear setup, execution, and verification phases +1. **Don't test the framework**: Trust that pandas, requests, etc. work correctly +1. **Mock at boundaries**: Mock HTTP calls and file I/O, not internal functions +1. **Keep integration tests focused**: Test critical paths only, use small queries ## Adding New Tests When adding functionality, follow this pattern: 1. **Add unit tests first**: Test the new function/class in isolation -2. **Use TDD when possible**: Write failing test, implement code, verify it passes -3. **Add integration test if needed**: For user-facing features, add end-to-end test -4. **Update this README**: Document any new fixtures or utilities you create +1. **Use TDD when possible**: Write failing test, implement code, verify it passes +1. **Add integration test if needed**: For user-facing features, add end-to-end test +1. **Update this README**: Document any new fixtures or utilities you create Example workflow: + ```bash # Create test file touch tests/download/unit/test_new_feature.py diff --git a/tests/cache/test_lru_eviction.py b/tests/cache/test_lru_eviction.py index 55accf3..6b3d179 100644 --- a/tests/cache/test_lru_eviction.py +++ b/tests/cache/test_lru_eviction.py @@ -3,7 +3,7 @@ import io import json import zipfile -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path from unittest.mock import patch @@ -36,7 +36,7 @@ def _make_manifest_entry(filename: str, downloaded_at: str) -> dict: def _ts(year: int, month: int, day: int) -> str: - return datetime(year, month, day, tzinfo=timezone.utc).strftime(ISO_FORMAT) + return datetime(year, month, day, tzinfo=UTC).strftime(ISO_FORMAT) def test_keeps_n_most_recent(tmp_path: Path) -> None: diff --git a/tests/download/unit/test_deflate64.py b/tests/download/unit/test_deflate64.py index 63a1c3f..7c2c5f6 100644 --- a/tests/download/unit/test_deflate64.py +++ b/tests/download/unit/test_deflate64.py @@ -20,7 +20,7 @@ ) -def _create_deflate64_zip(files: dict[str, bytes]) -> bytes: # noqa: PLR0915 # binary ZIP layout is inherently statement-heavy +def _create_deflate64_zip(files: dict[str, bytes]) -> bytes: """Create a ZIP archive using Deflate64 (type 9) compression. Manually constructs the ZIP binary format since Python's ``zipfile`` diff --git a/tests/download/unit/test_version_discovery.py b/tests/download/unit/test_version_discovery.py index 86e57b7..f844f9b 100644 --- a/tests/download/unit/test_version_discovery.py +++ b/tests/download/unit/test_version_discovery.py @@ -331,7 +331,7 @@ class TestDiscoverLatestVersionEdgeCases: """Edge cases for discover_latest_version.""" def test_bad_xml_in_200_raises_valueerror(self, _mock_http): - """200 with unparseable XML should raise ValueError.""" + """200 with unparsable XML should raise ValueError.""" _mock_http.return_value = (200, "= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version < '3.11'", + "python_full_version < '3.12'", ] [manifest] @@ -52,7 +51,6 @@ version = "25.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6e/00/2432bb2d445b39b5407f0a90e01b9a271475eea7caf913d7a86bcb956385/cattrs-25.3.0.tar.gz", hash = "sha256:1ac88d9e5eda10436c4517e390a4142d88638fe682c436c93db7ce4a277b884a", size = 509321, upload-time = "2025-10-07T12:26:08.737Z" } @@ -84,22 +82,6 @@ version = "3.4.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, - { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, - { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, - { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, - { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, - { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, - { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, - { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, - { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, - { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, - { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, @@ -194,18 +176,6 @@ version = "7.13.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b6/45/2c665ca77ec32ad67e25c77daf1cee28ee4558f3bc571cdbaf88a00b9f23/coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936", size = 820905, upload-time = "2025-12-08T13:14:38.055Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/08/bdd7ccca14096f7eb01412b87ac11e5d16e4cb54b6e328afc9dee8bdaec1/coverage-7.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:02d9fb9eccd48f6843c98a37bd6817462f130b86da8660461e8f5e54d4c06070", size = 217979, upload-time = "2025-12-08T13:12:14.505Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f0/d1302e3416298a28b5663ae1117546a745d9d19fde7e28402b2c5c3e2109/coverage-7.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:367449cf07d33dc216c083f2036bb7d976c6e4903ab31be400ad74ad9f85ce98", size = 218496, upload-time = "2025-12-08T13:12:16.237Z" }, - { url = "https://files.pythonhosted.org/packages/07/26/d36c354c8b2a320819afcea6bffe72839efd004b98d1d166b90801d49d57/coverage-7.13.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cdb3c9f8fef0a954c632f64328a3935988d33a6604ce4bf67ec3e39670f12ae5", size = 245237, upload-time = "2025-12-08T13:12:17.858Z" }, - { url = "https://files.pythonhosted.org/packages/91/52/be5e85631e0eec547873d8b08dd67a5f6b111ecfe89a86e40b89b0c1c61c/coverage-7.13.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d10fd186aac2316f9bbb46ef91977f9d394ded67050ad6d84d94ed6ea2e8e54e", size = 247061, upload-time = "2025-12-08T13:12:19.132Z" }, - { url = "https://files.pythonhosted.org/packages/0f/45/a5e8fa0caf05fbd8fa0402470377bff09cc1f026d21c05c71e01295e55ab/coverage-7.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f88ae3e69df2ab62fb0bc5219a597cb890ba5c438190ffa87490b315190bb33", size = 248928, upload-time = "2025-12-08T13:12:20.702Z" }, - { url = "https://files.pythonhosted.org/packages/f5/42/ffb5069b6fd1b95fae482e02f3fecf380d437dd5a39bae09f16d2e2e7e01/coverage-7.13.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4be718e51e86f553bcf515305a158a1cd180d23b72f07ae76d6017c3cc5d791", size = 245931, upload-time = "2025-12-08T13:12:22.243Z" }, - { url = "https://files.pythonhosted.org/packages/95/6e/73e809b882c2858f13e55c0c36e94e09ce07e6165d5644588f9517efe333/coverage-7.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a00d3a393207ae12f7c49bb1c113190883b500f48979abb118d8b72b8c95c032", size = 246968, upload-time = "2025-12-08T13:12:23.52Z" }, - { url = "https://files.pythonhosted.org/packages/87/08/64ebd9e64b6adb8b4a4662133d706fbaccecab972e0b3ccc23f64e2678ad/coverage-7.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a7b1cd820e1b6116f92c6128f1188e7afe421c7e1b35fa9836b11444e53ebd9", size = 244972, upload-time = "2025-12-08T13:12:24.781Z" }, - { url = "https://files.pythonhosted.org/packages/12/97/f4d27c6fe0cb375a5eced4aabcaef22de74766fb80a3d5d2015139e54b22/coverage-7.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:37eee4e552a65866f15dedd917d5e5f3d59805994260720821e2c1b51ac3248f", size = 245241, upload-time = "2025-12-08T13:12:28.041Z" }, - { url = "https://files.pythonhosted.org/packages/0c/94/42f8ae7f633bf4c118bf1038d80472f9dade88961a466f290b81250f7ab7/coverage-7.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62d7c4f13102148c78d7353c6052af6d899a7f6df66a32bddcc0c0eb7c5326f8", size = 245847, upload-time = "2025-12-08T13:12:29.337Z" }, - { url = "https://files.pythonhosted.org/packages/a8/2f/6369ca22b6b6d933f4f4d27765d313d8914cc4cce84f82a16436b1a233db/coverage-7.13.0-cp310-cp310-win32.whl", hash = "sha256:24e4e56304fdb56f96f80eabf840eab043b3afea9348b88be680ec5986780a0f", size = 220573, upload-time = "2025-12-08T13:12:30.905Z" }, - { url = "https://files.pythonhosted.org/packages/f1/dc/a6a741e519acceaeccc70a7f4cfe5d030efc4b222595f0677e101af6f1f3/coverage-7.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:74c136e4093627cf04b26a35dab8cbfc9b37c647f0502fc313376e11726ba303", size = 221509, upload-time = "2025-12-08T13:12:32.09Z" }, { url = "https://files.pythonhosted.org/packages/f1/dc/888bf90d8b1c3d0b4020a40e52b9f80957d75785931ec66c7dfaccc11c7d/coverage-7.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820", size = 218104, upload-time = "2025-12-08T13:12:33.333Z" }, { url = "https://files.pythonhosted.org/packages/8d/ea/069d51372ad9c380214e86717e40d1a743713a2af191cfba30a0911b0a4a/coverage-7.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f", size = 218606, upload-time = "2025-12-08T13:12:34.498Z" }, { url = "https://files.pythonhosted.org/packages/68/09/77b1c3a66c2aa91141b6c4471af98e5b1ed9b9e6d17255da5eb7992299e3/coverage-7.13.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96", size = 248999, upload-time = "2025-12-08T13:12:36.02Z" }, @@ -310,18 +280,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, ] -[[package]] -name = "exceptiongroup" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, -] - [[package]] name = "execnet" version = "2.1.2" @@ -400,16 +358,6 @@ version = "1.0.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3e/f3/41bb2901543abe7aad0b0b0284ae5854bb75f848cf406bf8a046bf525f67/inflate64-1.0.4.tar.gz", hash = "sha256:b398c686960c029777afc0ed281a86f66adb956cfc3fbf6667cc6453f7b407ce", size = 902542, upload-time = "2025-11-28T10:55:52.641Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/d5/5c13cfc7954ed716ae0e5e64c4f54be43f8c145b546472b67803feaa18a4/inflate64-1.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f1a47837d4322e0684824f91eb635aa6fd1967584140c478b0a1aca7b11740d6", size = 58602, upload-time = "2025-11-28T10:54:21.346Z" }, - { url = "https://files.pythonhosted.org/packages/33/57/4d740b677cda81ec6f47c05b502ed15103c8a7d9c3e91ee93352d46fe69c/inflate64-1.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8600478542e2354d1ee7b5c57c957006cabacd8b787b4046951f487a2216e5c0", size = 35856, upload-time = "2025-11-28T10:54:22.629Z" }, - { url = "https://files.pythonhosted.org/packages/17/cd/ec3c058283706a43ab790e8d611a3a787a4f4cc4ae3faeafba6e2e216e36/inflate64-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fb2b5a62579d074f38352a3494c3c6ac1a90516b75c5793c39303547f1fea925", size = 36007, upload-time = "2025-11-28T10:54:23.685Z" }, - { url = "https://files.pythonhosted.org/packages/24/83/90f7086f8078057a090db43459e478dc45e2d5ce2509f9c6a6a08100efa0/inflate64-1.0.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dcfafc572a642215894af1ec8d05949fa35eb7cb36d053aa97b11eccf1ae579e", size = 95055, upload-time = "2025-11-28T10:54:24.864Z" }, - { url = "https://files.pythonhosted.org/packages/95/d3/4f760f095ce8a9494d441b96a8735346141dd24f52fa573c971c0da1c958/inflate64-1.0.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cb93159cb60aee8cab62541aa70e4c460f13359660a27a1a486518bba0153535", size = 96645, upload-time = "2025-11-28T10:54:26.308Z" }, - { url = "https://files.pythonhosted.org/packages/8c/2a/78ab2fcb02c13e3c8c93c2d82bf5eec1862b428bc6177dcc76ac4044408d/inflate64-1.0.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:89126ceb4d96e76842f4697017a9a3e750c34e029ddb360b3d8ca79a648d47f6", size = 92933, upload-time = "2025-11-28T10:54:27.563Z" }, - { url = "https://files.pythonhosted.org/packages/c5/1b/c9a2d84fc117dddee0749dc1b3ab9ed725bf92e866ef0ede0945a5128ef0/inflate64-1.0.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f70e6692617ec82500b203eefac8765302298ce7e73584fcf995bb9e23184530", size = 95898, upload-time = "2025-11-28T10:54:28.91Z" }, - { url = "https://files.pythonhosted.org/packages/bd/85/5879fa47122c7d5b563c6dacbd4a782bd9464405f69d46af01e02d4a3907/inflate64-1.0.4-cp310-cp310-win32.whl", hash = "sha256:d08cdda33341b4f992af60c12dc60e370e9993b80a936c17244a602711eeb727", size = 32940, upload-time = "2025-11-28T10:54:30.385Z" }, - { url = "https://files.pythonhosted.org/packages/a3/59/cef1b3505dc33d8cb9d115481923dec1de1372d29ac278622feecf9c03a1/inflate64-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:950dd7fe53474df5f4699b8f099980027e812d55fd82d8e167d599822c3d27d6", size = 35541, upload-time = "2025-11-28T10:54:31.837Z" }, - { url = "https://files.pythonhosted.org/packages/72/13/d964fbfceeee6752c36c45645e5e9a9ef0dd70d4ce64e5e7316822e43382/inflate64-1.0.4-cp310-cp310-win_arm64.whl", hash = "sha256:bad20de249d6336793f6267880668dbb286ca5c6e6991795aa6344c817588068", size = 33460, upload-time = "2025-11-28T10:54:32.954Z" }, { url = "https://files.pythonhosted.org/packages/39/e4/2fc07d71cf863ed4167e7d3eb7f89de0341ffe3ed3a62ff6cc4123bdbda6/inflate64-1.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bccda9815b27623e805a34ee3ee4f46c93f0cc7ac621f9834d75f033fd79c27a", size = 58595, upload-time = "2025-11-28T10:54:34.338Z" }, { url = "https://files.pythonhosted.org/packages/53/77/1119bb53e8f4c9c77f2a5e3ab7d8c3e905fcfc9912073962b9b4cbf72118/inflate64-1.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c11e2a3cb9d9b49620c9b0c806dd0c55daec3b6bb665299b770a68f01bfc5432", size = 35854, upload-time = "2025-11-28T10:54:35.796Z" }, { url = "https://files.pythonhosted.org/packages/de/40/8b028a731f6fabbb49069a58f1aa5c3332688b57dcb8726f9e596661ce5b/inflate64-1.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e42def03ace8c58fd50b0df4f40241c45a2314c3876d020cce24acf958323c98", size = 36011, upload-time = "2025-11-28T10:54:37.208Z" }, @@ -507,17 +455,6 @@ version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, - { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, - { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, - { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, @@ -708,7 +645,6 @@ dependencies = [ { name = "griffe" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/24/75/d30af27a2906f00eb90143470272376d728521997800f5dce5b340ba35bc/mkdocstrings_python-2.0.1.tar.gz", hash = "sha256:843a562221e6a471fefdd4b45cc6c22d2607ccbad632879234fa9692e9cf7732", size = 199345, upload-time = "2025-12-03T14:26:11.755Z" } wheels = [ @@ -724,79 +660,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, ] -[[package]] -name = "numpy" -version = "2.2.6" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, - { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, - { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, - { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, - { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, - { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, - { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, - { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, - { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, - { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, - { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, - { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, - { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, - { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, - { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, - { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, - { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, - { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, - { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, - { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, - { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, - { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, - { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, - { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, - { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, - { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, - { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, - { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, - { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, - { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, - { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, - { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, - { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, - { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, - { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, - { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, - { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, - { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, - { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, -] - [[package]] name = "numpy" version = "2.3.5" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" }, @@ -896,6 +763,7 @@ dev = [ { name = "pytest" }, { name = "pytest-mock" }, { name = "ruff" }, + { name = "ty" }, ] docs = [ { name = "mkdocs" }, @@ -929,6 +797,7 @@ dev = [ { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-mock", specifier = ">=3.15.1" }, { name = "ruff", specifier = ">=0.14.0" }, + { name = "ty", specifier = ">=0.0.33" }, ] docs = [ { name = "mkdocs", specifier = ">=1.5.0" }, @@ -978,21 +847,13 @@ name = "pandas" version = "2.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, { name = "python-dateutil" }, { name = "pytz" }, { name = "tzdata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, - { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, - { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, - { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, - { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, - { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, - { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, @@ -1084,13 +945,6 @@ version = "22.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151, upload-time = "2025-10-24T12:30:00.762Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/9b/cb3f7e0a345353def531ca879053e9ef6b9f38ed91aebcf68b09ba54dec0/pyarrow-22.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:77718810bd3066158db1e95a63c160ad7ce08c6b0710bc656055033e39cdad88", size = 34223968, upload-time = "2025-10-24T10:03:31.21Z" }, - { url = "https://files.pythonhosted.org/packages/6c/41/3184b8192a120306270c5307f105b70320fdaa592c99843c5ef78aaefdcf/pyarrow-22.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:44d2d26cda26d18f7af7db71453b7b783788322d756e81730acb98f24eb90ace", size = 35942085, upload-time = "2025-10-24T10:03:38.146Z" }, - { url = "https://files.pythonhosted.org/packages/d9/3d/a1eab2f6f08001f9fb714b8ed5cfb045e2fe3e3e3c0c221f2c9ed1e6d67d/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b9d71701ce97c95480fecb0039ec5bb889e75f110da72005743451339262f4ce", size = 44964613, upload-time = "2025-10-24T10:03:46.516Z" }, - { url = "https://files.pythonhosted.org/packages/46/46/a1d9c24baf21cfd9ce994ac820a24608decf2710521b29223d4334985127/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:710624ab925dc2b05a6229d47f6f0dac1c1155e6ed559be7109f684eba048a48", size = 47627059, upload-time = "2025-10-24T10:03:55.353Z" }, - { url = "https://files.pythonhosted.org/packages/3a/4c/f711acb13075c1391fd54bc17e078587672c575f8de2a6e62509af026dcf/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f963ba8c3b0199f9d6b794c90ec77545e05eadc83973897a4523c9e8d84e9340", size = 47947043, upload-time = "2025-10-24T10:04:05.408Z" }, - { url = "https://files.pythonhosted.org/packages/4e/70/1f3180dd7c2eab35c2aca2b29ace6c519f827dcd4cfeb8e0dca41612cf7a/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd0d42297ace400d8febe55f13fdf46e86754842b860c978dfec16f081e5c653", size = 50206505, upload-time = "2025-10-24T10:04:15.786Z" }, - { url = "https://files.pythonhosted.org/packages/80/07/fea6578112c8c60ffde55883a571e4c4c6bc7049f119d6b09333b5cc6f73/pyarrow-22.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:00626d9dc0f5ef3a75fe63fd68b9c7c8302d2b5bbc7f74ecaedba83447a24f84", size = 28101641, upload-time = "2025-10-24T10:04:22.57Z" }, { url = "https://files.pythonhosted.org/packages/2e/b7/18f611a8cdc43417f9394a3ccd3eace2f32183c08b9eddc3d17681819f37/pyarrow-22.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3e294c5eadfb93d78b0763e859a0c16d4051fc1c5231ae8956d61cb0b5666f5a", size = 34272022, upload-time = "2025-10-24T10:04:28.973Z" }, { url = "https://files.pythonhosted.org/packages/26/5c/f259e2526c67eb4b9e511741b19870a02363a47a35edbebc55c3178db22d/pyarrow-22.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:69763ab2445f632d90b504a815a2a033f74332997052b721002298ed6de40f2e", size = 35995834, upload-time = "2025-10-24T10:04:35.467Z" }, { url = "https://files.pythonhosted.org/packages/50/8d/281f0f9b9376d4b7f146913b26fac0aa2829cd1ee7e997f53a27411bbb92/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b41f37cabfe2463232684de44bad753d6be08a7a072f6a83447eeaf0e4d2a215", size = 45030348, upload-time = "2025-10-24T10:04:43.366Z" }, @@ -1163,12 +1017,10 @@ version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ @@ -1254,15 +1106,6 @@ version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, - { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, @@ -1440,6 +1283,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, ] +[[package]] +name = "ty" +version = "0.0.49" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/8d/37cb91808069509d43a2a11743e12f1e854fd808dbef2203309d256718cd/ty-0.0.49.tar.gz", hash = "sha256:0a027bd0c9c75d035641a365d087ad883446057f9be0b9826251c2aecafbf145", size = 5884753, upload-time = "2026-06-12T03:08:20.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/de/9237c6a96356612dd0393db1e94cf21f903616adf3a3701bf3da6e4adc92/ty-0.0.49-py3-none-linux_armv6l.whl", hash = "sha256:12c0c4310b936d762a8586c210b53d4fa4bb361a04429afa89bf84b922e5e065", size = 11834671, upload-time = "2026-06-12T03:07:53.062Z" }, + { url = "https://files.pythonhosted.org/packages/8f/15/daf5a14a5e07012277d450c75325c94614e2acfec4c620c881486118c410/ty-0.0.49-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:737bfdc2caf9712a8580944dcdc80a450a37a4f2bc83c8fa9b7433b374f9e471", size = 11589570, upload-time = "2026-06-12T03:08:25.779Z" }, + { url = "https://files.pythonhosted.org/packages/7d/58/30bdf98436488aca25f0763bf7f92a061528d42461b686453029e845e4c5/ty-0.0.49-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ab90c1baf3b1701d282fce4b02fa552a962d109f8972c46ef6b22429503bfea4", size = 10985236, upload-time = "2026-06-12T03:08:36.664Z" }, + { url = "https://files.pythonhosted.org/packages/22/45/ece503e4a1396e13a1a9a0cde51afe476a6506a1d557eeadf8ad45c83bc0/ty-0.0.49-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ce8ecf6ba6fc79bd137cc0557a754f7e5f2dfe9436412551d480d680e248ad", size = 11504302, upload-time = "2026-06-12T03:08:01.664Z" }, + { url = "https://files.pythonhosted.org/packages/17/dc/5d09333d289dfbca1804eaade125c9e8a1a992a2a592a8b80c5e9b589ca9/ty-0.0.49-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:10d85c6865c984e78661e0bd20b180514b4a289739224e84816e342bdf381e04", size = 11626629, upload-time = "2026-06-12T03:08:06.844Z" }, + { url = "https://files.pythonhosted.org/packages/f2/36/155f41c9dd7237c4b609211f29f77755a139ee6218605dadc7fe21d5e3c8/ty-0.0.49-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d96a67a206619e01fa92f35a22267ec634bba62be24b1d0e947020cc179995b", size = 12074481, upload-time = "2026-06-12T03:08:09.643Z" }, + { url = "https://files.pythonhosted.org/packages/96/4c/998ee13cd5045f1f8b36982de7343163832ac53f27debe01b0de0e8bd968/ty-0.0.49-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3de9f648564e0a66344ef397770387cb0d093735f8679d2c5a08a4741e79814d", size = 12678042, upload-time = "2026-06-12T03:08:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/85/c9/9a505aba85c41ce54cbcaa14f8d79aa084b86151d2d70df11c4655b92898/ty-0.0.49-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5779179ab397d15f8c9dbb8f506ec1b1745f54eac639982f76ef3ce538943b50", size = 12316194, upload-time = "2026-06-12T03:08:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b8/ded37fb93503294abbc83c36470bb1413bea05048b745881d4470b518a06/ty-0.0.49-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:792d4974e93cc09bd32f934586080bbbe21b8e777099cb521cb2de18b68a49f0", size = 12145507, upload-time = "2026-06-12T03:07:56.505Z" }, + { url = "https://files.pythonhosted.org/packages/2f/07/392e80d78f02445f695b815bb9eb0fffacda68b03faee38c900f7b990815/ty-0.0.49-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:727bda86deb136073e525c2e78d60e38aedcce5d80579170844a52bbf7c1440d", size = 12365967, upload-time = "2026-06-12T03:08:12.553Z" }, + { url = "https://files.pythonhosted.org/packages/50/d3/31b0c2a7fbedd3373e389cb1d81b8d2128f6f868fafb46557736a6f9aca8/ty-0.0.49-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4f2fc2bc4a8d2ff1cca59fd94772cabdfec4062d47a0b3a0784be46d94d0540b", size = 11475283, upload-time = "2026-06-12T03:08:28.334Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5b/329e101638920b468a3bb63059c9f66ef99b44aac501222c44832a507321/ty-0.0.49-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3724bd9badef333321578b6a941fbc571ebf49141ec2356a8590fbe4c9aa588d", size = 11645343, upload-time = "2026-06-12T03:08:15.246Z" }, + { url = "https://files.pythonhosted.org/packages/a9/76/c897e615e32f80ca81c8c1bc49b9a1f72ff9e3cfea0f8345ba505fe28472/ty-0.0.49-py3-none-musllinux_1_2_i686.whl", hash = "sha256:166c6eb52ee4af3c5a9bb267d165d93000daa55c6758cd8ff3199741fb75917d", size = 11725585, upload-time = "2026-06-12T03:08:33.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/e1/fdb42ee239f618800842681af5bb8598117e74512c10974a8b7b9086a898/ty-0.0.49-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:91e81d832c287b05782ee32eb1b801f62c1fa08df37d589d2b88c3f1d51c9731", size = 12237261, upload-time = "2026-06-12T03:08:31.105Z" }, + { url = "https://files.pythonhosted.org/packages/98/0f/a2d6a5fc9d0786cbeb3c200786da4e18c203589be3984bb5def83ca92320/ty-0.0.49-py3-none-win32.whl", hash = "sha256:7186af5ca9829d1f5d8916bcf767b8e819bfbf61b1b8ec843bb3fc699cb502e1", size = 11100789, upload-time = "2026-06-12T03:07:59.092Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9d/473ac8bc57b5a2d121da893bf9dd74a118efb19a01d711df1a6e397f05cc/ty-0.0.49-py3-none-win_amd64.whl", hash = "sha256:ae2142fc126a01effcca0c222908b0e6654b5ba1266d4e4d406e4866aef8e1d1", size = 12204644, upload-time = "2026-06-12T03:08:04.327Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a2/8959249da951ba3977fee20e688d28678b8a1d30a9ed4464228a85d45853/ty-0.0.49-py3-none-win_arm64.whl", hash = "sha256:75d5e2e7649765f31f4bed6c8adb149a75b18edd3fa6336dac4d0efc1a66466f", size = 11558965, upload-time = "2026-06-12T03:08:23.012Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -1488,7 +1356,6 @@ dependencies = [ { name = "filelock" }, { name = "platformdirs" }, { name = "python-discovery" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/97/c5/aff062c66b42e2183201a7ace10c6b2e959a9a16525c8e8ca8e59410d27a/virtualenv-21.2.1.tar.gz", hash = "sha256:b66ffe81301766c0d5e2208fc3576652c59d44e7b731fc5f5ed701c9b537fa78", size = 5844770, upload-time = "2026-04-09T18:47:11.482Z" } wheels = [ @@ -1501,9 +1368,6 @@ version = "6.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, - { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, - { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, @@ -1513,8 +1377,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, - { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },