diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e9bf488..f1271d7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,4 +41,4 @@ jobs: - name: Publish to PyPI (uv) env: UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }} - run: uv publish \ No newline at end of file + run: uv publish diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..95b20f5 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,36 @@ +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 }} + + - name: Install dependencies + run: uv sync --all-groups + + - name: Run unit tests + run: uv run pytest tests/ -n auto -m "not integration" -v + + - 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 a6827e8..b07b43d 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,5 @@ cython_debug/ .idea/ .DS_Store /src/oda_reader/dev_tests.py +/docs/site +/docs/plans diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..5bd0faf --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,119 @@ +# Pre-commit hooks for ODA Reader Package +# +# Installation: +# 1. Install pre-commit: `uv sync --group dev` +# 2. Install 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` + +repos: + # ============================================================================ + # Ruff - Fast Python linter and formatter (replaces black, isort, flake8) + # ============================================================================ + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.4 + hooks: + # Run the linter + - id: ruff + args: [--fix] + # Run the formatter + - id: ruff-format + + # ============================================================================ + # Built-in pre-commit hooks for file quality + # ============================================================================ + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + # Remove trailing whitespace + - 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 + - id: check-case-conflict + + # Check for merge conflict strings + - id: check-merge-conflict + + # Check for debug statements (pdb, breakpoint, etc.) + - id: debug-statements + + # Validate Python syntax + - id: check-ast + + # Check for proper shebang formatting + - id: check-shebang-scripts-are-executable + + # 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 + - id: check-json + + # Pretty-format JSON files + - id: pretty-format-json + args: [--autofix, --indent=2, --no-sort-keys] + exclude: | + (?x)^( + \.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] + + # ============================================================================ + # Security checks + # ============================================================================ + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + # Detect private keys + - id: detect-private-key + + # ============================================================================ + # Python-specific checks + # ============================================================================ + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + # Check for common Python mistakes + - id: check-builtin-literals + + # Check docstring is first + - id: check-docstring-first + + # Validate pyproject.toml + - id: check-toml diff --git a/CHANGELOG.md b/CHANGELOG.md index ed95604..eb357d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,10 +19,10 @@ a type error. for the bulk Multisystem data. This is a small fix to address that. ## 1.1.4 (2025-04-22) -- fix small cache bug +- fix small cache bug ## 1.1.3 (2025-04-22) -- Small caching improvements +- Small caching improvements ## 1.1.2 (2025-04-22) - Extends caching to bulk downloaded files. @@ -30,7 +30,7 @@ for the bulk Multisystem data. This is a small fix to address that. ## 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. +certain dataflows return `NoRecordsFound`, even though the query is valid for lower dataflows. This version of `oda_reader` defends against that. @@ -88,4 +88,4 @@ and the wrong schema (dac1) was loaded. ## 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. \ No newline at end of file +- This release includes tools to translate the API response into the old .Stat schema. diff --git a/README.md b/README.md index f014abc..8c3a340 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ The OECD DAC Data Importer the **OECD data explorer API** and bulk downloads. It allows for easy, programmatic access to OECD DAC data in python. It is designed for policy -analysts, data analysts, researchers and students who need easy and programmatic access to +analysts, data analysts, researchers and students who need easy and programmatic access to OECD DAC data. This documentation will walk you through how to set up and use ODA Reader. @@ -246,7 +246,7 @@ crs_data = download_crs( The filtering can get quite specific. For example, the following query gets disbursements for ODA grants from Germany to Nigeria for -primary education, provided through multilateral organisations, in +primary education, provided through multilateral organisations, in constant prices: ```python @@ -266,8 +266,8 @@ crs_data = download_crs( ) ``` -The data-explorer API can also return semi-aggregates, built from the CRS microdata. -That is the data that is shown online through the data-explorer. +The data-explorer API can also return semi-aggregates, built from the CRS microdata. +That is the data that is shown online through the data-explorer. You can get that view of the data using the ODA Reader package. However, the filters must be used to avoid double counting. @@ -540,7 +540,7 @@ When using ODA Reader, you can apply filters to refine the data you retrieve fro Filters allow you to specify subsets of data, making it easy to focus on the information that is most relevant to your needs. -Filters are specified as a dictionary, with keys representing the filter categories (such as donor, recipient, sector, etc.) and values representing the criteria to match, provided as single values (like a year, or a code), or lists of values (like multiple donors or multiple sectors). +Filters are specified as a dictionary, with keys representing the filter categories (such as donor, recipient, sector, etc.) and values representing the criteria to match, provided as single values (like a year, or a code), or lists of values (like multiple donors or multiple sectors). You can use the `get_available_filters()` function to see the available filter parameters that can be used for a specific dataset. Note that (for now) all filter values must be provided using diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..885acf8 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,53 @@ +# ODA Reader Documentation + +This directory contains the MkDocs documentation for ODA Reader. + +## Building Locally + +Install dependencies: + +```bash +cd .. # back to project root +uv sync --group docs +``` + +Serve locally: + +```bash +cd docs +uv run mkdocs serve +``` + +Visit http://127.0.0.1:8000 + +## Building for Production + +```bash +cd docs +uv run mkdocs build +``` + +Output is in `site/` directory. + +## Testing Documentation Examples + +Run example test scripts: + +```bash +cd .. # back to project root +uv run python docs/examples/getting_started_examples.py +uv run python docs/examples/filtering_examples.py +``` + +All examples should pass before committing documentation updates. + +## Documentation Structure + +- `mkdocs.yml` - MkDocs configuration +- `docs/` - All markdown content +- `examples/` - Test scripts for documentation examples +- `plans/` - Design documents and implementation plans + +## Deployment + +(Add deployment instructions for your hosting platform here) diff --git a/docs/docs/advanced.md b/docs/docs/advanced.md new file mode 100644 index 0000000..0b8977c --- /dev/null +++ b/docs/docs/advanced.md @@ -0,0 +1,340 @@ +# Advanced Topics + +This page covers advanced features and customization options for power users. + +## Using QueryBuilder Directly + +`QueryBuilder` is the internal class that constructs SDMX API queries. You can use it directly for custom queries: + +```python +from oda_reader import QueryBuilder + +qb = QueryBuilder() + +# Build a custom DAC1 filter +filter_string = qb.build_dac1_filter( + donor="USA", + measure="1010", + flow_type="1140" +) + +print(filter_string) +# Output: "USA.....1010.1140..." +``` + +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, recipient, flow_type, measure, unit_measure, price_base)` +- `build_dac2a_filter(donor, recipient, measure, price_base, ...)` +- `build_crs_filter(donor, recipient, sector, channel, modality, microdata, ...)` +- `build_multisystem_filter(donor, channel, flow_type, ...)` + +Each method returns a filter string suitable for the SDMX API. + +## Dataflow Version Handling + +OECD occasionally changes dataflow versions (schema updates). ODA Reader handles this automatically with version fallback. + +### Automatic Fallback + +When a dataflow version returns 404 (not found), ODA Reader automatically: + +1. Tries the configured version (e.g., `1.0`) +2. If 404, retries with `0.9` +3. Continues decrementing: `0.8`, `0.7`, `0.6` +4. Returns data from first successful version (up to 5 attempts) + +This means your code keeps working even when OECD makes breaking schema changes. + +**Example**: + +```python +from oda_reader import download_dac1 + +# ODA Reader will automatically try: +# 1.0 -> 404 +# 0.9 -> 404 +# 0.8 -> Success! Returns data with version 0.8 +data = download_dac1(start_year=2022, end_year=2022) +``` + +You'll see a message indicating which version succeeded. + +### Manual Version Override + +You can specify an exact dataflow version: + +```python +# Force use of version 0.8 +data = download_dac1( + start_year=2022, + end_year=2022, + dataflow_version="0.8" +) +``` + +**When to override**: +- You know the correct version for reproducibility +- Debugging version-specific issues +- Avoiding automatic fallback (for performance) + +**Available for**: +- `download_dac1(dataflow_version=...)` +- `download_dac2a(dataflow_version=...)` +- `download_crs(dataflow_version=...)` +- `download_multisystem(dataflow_version=...)` + +## API Version Differences + +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) + +You generally don't need to worry about this - ODA Reader handles it automatically. + +## Custom Rate Limiting Strategies + +Beyond basic rate limiting configuration, you can implement custom strategies: + +### Disable Rate Limiting (Use Carefully) + +```python +from oda_reader import API_RATE_LIMITER + +# Effectively disable (very high limit) +API_RATE_LIMITER.max_calls = 1000 +API_RATE_LIMITER.period = 1 +``` + +**Warning**: This may get you blocked by OECD's servers. Only use for testing or if you have permission. + +### Dynamic Rate Limiting + +```python +# Start conservative +API_RATE_LIMITER.max_calls = 10 +API_RATE_LIMITER.period = 60 + +# Make some calls... + +# Adjust based on response times or errors +if experiencing_slowdowns: + API_RATE_LIMITER.max_calls = 5 # Slow down +``` + +### Check Rate Limiter State + +```python +# Access internal state (undocumented, subject to change) +print(f"Calls made: {len(API_RATE_LIMITER.call_times)}") +print(f"Max calls: {API_RATE_LIMITER.max_calls}") +``` + +## Combining Multiple Queries + +For complex analysis, you might combine multiple queries: + +```python +from oda_reader import download_dac1, download_dac2a +import pandas as pd + +# Get donor totals from DAC1 +donor_totals = download_dac1(start_year=2022, end_year=2022) + +# Get bilateral flows from DAC2a +bilateral = download_dac2a(start_year=2022, end_year=2022) + +# Merge for analysis +# (Note: ensure compatible schemas and codes) +combined = pd.merge( + donor_totals, + bilateral, + on=['donor', 'year'], + how='inner' +) +``` + +**Tips**: +- Use same `pre_process` and `dotstat_codes` settings for compatibility +- Column names and codes must align +- Filter carefully to avoid double-counting + +## Custom Schema Handling + +If you need custom schema translation beyond built-in options: + +### Access Raw Data and Translate Manually + +```python +# Get raw API data +data = download_dac1( + start_year=2022, + end_year=2022, + pre_process=False, + dotstat_codes=False +) + +# Apply custom transformations +data = data.rename(columns={'DONOR': 'donor_custom'}) +data['donor_custom'] = data['donor_custom'].map(my_custom_mapping) +``` + +### Load Schema Mapping Files + +```python +import json +from pathlib import Path + +# Find ODA Reader installation +import oda_reader +package_path = Path(oda_reader.__file__).parent + +# Load schema mapping +with open(package_path / 'schemas/mappings/dac1_mapping.json') as f: + mapping = json.load(f) + +# Use for custom translation +print(mapping['DONOR']['mapping']) +# {'1': 'AUS', '2': 'AUT', ...} +``` + +## Working with Large Datasets in Production + +For production pipelines with large datasets: + +### Use Bulk Downloads + Local Storage + +```python +from oda_reader import bulk_download_crs +import pandas as pd + +# Download once, save locally +bulk_download_crs(save_to_path="/data/crs_full.parquet") + +# In your pipeline, read from local file (fast) +def get_crs_data(): + return pd.read_parquet("/data/crs_full.parquet") +``` + +### Refresh Strategy + +```python +from pathlib import Path +from datetime import datetime, timedelta + +def refresh_if_old(file_path, max_age_days=7): + """Re-download if file is older than max_age_days""" + path = Path(file_path) + + if not path.exists(): + print("File doesn't exist, downloading...") + bulk_download_crs(save_to_path=file_path) + return + + file_age = datetime.now() - datetime.fromtimestamp(path.stat().st_mtime) + + if file_age > timedelta(days=max_age_days): + print(f"File is {file_age.days} days old, refreshing...") + bulk_download_crs(save_to_path=file_path) + else: + print(f"File is recent ({file_age.days} days old), using cached version") + +# Use in pipeline +refresh_if_old("/data/crs_full.parquet", max_age_days=7) +crs_data = pd.read_parquet("/data/crs_full.parquet") +``` + +### Memory-Efficient Aggregation + +```python +# Process bulk CRS in chunks, aggregate results +sector_totals = {} + +for chunk in bulk_download_crs(as_iterator=True): + # Aggregate by sector + sector_sums = chunk.groupby('purpose_code')['usd_commitment'].sum() + + # Accumulate + for sector, amount in sector_sums.items(): + sector_totals[sector] = sector_totals.get(sector, 0) + amount + +print(f"Total sectors: {len(sector_totals)}") +``` + +## Debugging Tips + +### Enable Verbose Logging + +ODA Reader doesn't have built-in verbose logging yet, but you can inspect behavior: + +```python +# Check cache behavior +from oda_reader import get_cache_dir, get_http_cache_info + +print(f"Cache location: {get_cache_dir()}") +print(f"HTTP cache info: {get_http_cache_info()}") + +# Clear caches to force fresh downloads +from oda_reader import clear_cache, clear_http_cache +clear_cache() +clear_http_cache() +``` + +### Test with Small Queries First + +```python +# Test with single year, single donor +test_data = download_crs( + start_year=2022, + end_year=2022, + filters={"donor": "USA"} +) + +print(f"Columns: {list(test_data.columns)}") +print(f"Shape: {test_data.shape}") +print(test_data.head()) +``` + +### Compare API vs. Bulk Schema + +```python +# Download same data both ways +api_data = download_crs( + start_year=2022, + end_year=2022, + filters={"donor": "USA"} +) + +bulk_data = bulk_download_crs() +bulk_filtered = bulk_data[ + (bulk_data['Year'] == 2022) & + (bulk_data['DonorCode'] == 'USA') +] + +print("API columns:", list(api_data.columns)[:10]) +print("Bulk columns:", list(bulk_filtered.columns)[:10]) +``` + +## Next Steps + +- **[API Reference](api-reference.md)** - Complete function signatures and parameters +- **[Getting Started](getting-started.md)** - Return to basics if needed +- **GitHub Issues** - Report bugs or request features at the ODA Reader repository diff --git a/docs/docs/api-reference.md b/docs/docs/api-reference.md new file mode 100644 index 0000000..589df30 --- /dev/null +++ b/docs/docs/api-reference.md @@ -0,0 +1,130 @@ +# API Reference + +This page provides automatically generated API documentation for ODA Reader's public functions and classes. + +## Data Download Functions + +::: oda_reader.download_dac1 + options: + show_root_heading: true + show_source: false + +::: oda_reader.download_dac2a + options: + show_root_heading: true + show_source: false + +::: oda_reader.download_crs + options: + show_root_heading: true + show_source: false + +::: oda_reader.download_multisystem + options: + show_root_heading: true + show_source: false + +::: oda_reader.download_aiddata + options: + show_root_heading: true + show_source: false + +## Bulk Download Functions + +::: oda_reader.bulk_download_crs + options: + show_root_heading: true + show_source: false + +::: oda_reader.bulk_download_multisystem + options: + show_root_heading: true + show_source: false + +::: oda_reader.download_crs_file + options: + show_root_heading: true + show_source: false + +## Utility Functions + +::: oda_reader.get_available_filters + options: + show_root_heading: true + show_source: false + +::: oda_reader.QueryBuilder + options: + show_root_heading: true + show_source: false + members: + - build_dac1_filter + - build_dac2a_filter + - build_crs_filter + - build_multisystem_filter + +## Cache Management + +::: oda_reader.get_cache_dir + options: + show_root_heading: true + show_source: false + +::: oda_reader.set_cache_dir + options: + show_root_heading: true + show_source: false + +::: oda_reader.reset_cache_dir + options: + show_root_heading: true + show_source: false + +::: oda_reader.clear_cache + options: + show_root_heading: true + show_source: false + +::: oda_reader.enable_http_cache + options: + show_root_heading: true + show_source: false + +::: oda_reader.disable_http_cache + options: + show_root_heading: true + show_source: false + +::: oda_reader.clear_http_cache + options: + show_root_heading: true + show_source: false + +::: oda_reader.get_http_cache_info + options: + show_root_heading: true + show_source: false + +## Rate Limiting + +::: oda_reader.common.RateLimiter + options: + show_root_heading: true + show_source: false + heading_level: 3 + +The global rate limiter instance is available as: + +```python +from oda_reader import API_RATE_LIMITER + +# Configure rate limiting +API_RATE_LIMITER.max_calls = 20 +API_RATE_LIMITER.period = 60 +``` + +See [Caching & Performance](caching.md#rate-limiting) for more details. + +## Getting Help + +For issues or feature requests, visit the [GitHub repository](https://github.com/ONEcampaign/oda_reader). diff --git a/docs/docs/bulk-downloads.md b/docs/docs/bulk-downloads.md new file mode 100644 index 0000000..73c2e1e --- /dev/null +++ b/docs/docs/bulk-downloads.md @@ -0,0 +1,255 @@ +# Bulk Downloads + +For large-scale analysis, bulk downloads are faster and more reliable than repeated API calls. ODA Reader provides bulk download functions for CRS, Multisystem, and AidData datasets. + +## 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 +- API queries are too slow or hitting rate limits +- 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 +- You only need recent data + +## CRS Bulk Downloads + +The full Creditor Reporting System dataset is available as a parquet file (~1GB compressed). ODA Reader can download and load it for you. + +### Download Full CRS + +```python +from oda_reader import bulk_download_crs + +# Download and return as DataFrame (loads ~1GB into memory) +crs_data = bulk_download_crs() + +print(f"Downloaded {len(crs_data)} rows") +print(f"Memory usage: {crs_data.memory_usage(deep=True).sum() / 1024**2:.1f} MB") +``` + +**Warning**: The full CRS is large. Loading it entirely into memory requires several GB of RAM. + +### Save to Disk Instead + +To avoid memory issues, save directly to disk: + +```python +# Save to a folder instead of loading into memory +bulk_download_crs(save_to_path="./data/crs_full.parquet") +``` + +Then load it separately with pandas when needed: + +```python +import pandas as pd + +# Load the saved file +crs_data = pd.read_parquet("./data/crs_full.parquet") +``` + +### Reduced Version (Smaller File) + +OECD provides a "reduced" version with fewer columns: + +```python +# Download reduced version (smaller file, fewer columns) +crs_reduced = bulk_download_crs(reduced_version=True) +``` + +Or save to disk: + +```python +bulk_download_crs( + save_to_path="./data/crs_reduced.parquet", + reduced_version=True +) +``` + +The reduced version omits some descriptive columns but retains all flow amounts and key dimensions. + +## Memory-Efficient Processing with Iterators + +For very large files, process in chunks to avoid loading the entire dataset into memory: + +```python +# Process in chunks (much lower memory usage) +for chunk in bulk_download_crs(as_iterator=True): + # chunk is a DataFrame with a subset of rows + + # Filter or aggregate each chunk + filtered = chunk[chunk['donor_code'] == 'USA'] + + # Save results or accumulate statistics + filtered.to_csv("usa_projects.csv", mode="a", header=False) +``` + +**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 + +**Combining with filtering**: + +```python +# Filter for education sector projects while streaming +education_count = 0 +education_amount = 0 + +for chunk in bulk_download_crs(as_iterator=True): + education = chunk[chunk['purpose_code'].str.startswith('11')] + education_count += len(education) + education_amount += education['usd_commitment'].sum() + +print(f"Education projects: {education_count}") +print(f"Total commitments: ${education_amount/1e9:.1f}B") +``` + +## Year-Specific CRS Files + +OECD also provides individual files for specific years: + +```python +from oda_reader import download_crs_file + +# Download 2022 CRS data only +crs_2022 = download_crs_file(year=2022) + +# Or save to disk +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 +- `"2000-01"`: 2000-2001 combined +- `"1995-99"`: 1995-1999 combined +- `"1973-94"`: 1973-1994 combined + +**Example**: + +```python +# Download historical data +crs_90s = download_crs_file(year="1995-99") +``` + +Year-specific files are much smaller than the full CRS, making them easier to work with. + +## Multisystem Bulk Download + +The full Multisystem dataset is available as a single parquet file: + +```python +from oda_reader import bulk_download_multisystem + +# Download and return as DataFrame +multisystem_data = bulk_download_multisystem() + +# Or save to disk +bulk_download_multisystem(save_to_path="./data/multisystem.parquet") +``` + +Multisystem is smaller than CRS, so memory is less of a concern. However, iterators are still supported: + +```python +# Process Multisystem in chunks +for chunk in bulk_download_multisystem(as_iterator=True): + # Process each chunk + pass +``` + +## AidData Download + +AidData (Chinese development finance) comes from an Excel file automatically downloaded and parsed: + +```python +from oda_reader import download_aiddata + +# Download full AidData dataset +aiddata = download_aiddata() + +# Filter by commitment year +aiddata_recent = download_aiddata(start_year=2015, end_year=2020) + +# Save to disk +download_aiddata(save_to_path="./data/aiddata.parquet") +``` + +**Note**: AidData filtering happens after download (Excel file is downloaded first, then filtered). It's not querying an API like the DAC datasets. + +## Important: Bulk Files Use .Stat Schema + +**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) + +**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. + +## Combining Bulk and API Downloads + +You can mix approaches: + +```python +# Download full CRS as bulk file +crs_full = bulk_download_crs() + +# Use API for recent updates or specific queries +crs_recent = download_crs( + start_year=2023, + end_year=2023, + filters={"donor": "USA"} +) + +# Combine if schemas match +# (you may need to harmonize column names first) +``` + +## Performance Comparison + +Approximate times (varies by network speed and OECD server load): + +| Method | Dataset Size | Time | +|--------|-------------|------| +| API download (filtered) | 10,000 rows | 10-30 seconds | +| API download (large query) | 100,000 rows | 2-5 minutes | +| Bulk download CRS | ~2 million rows | 1-2 minutes | +| Bulk + iterator (filter) | Process 2 million rows | 2-5 minutes | + +Bulk downloads are consistently fast regardless of query complexity, while API times vary significantly with query size. + +## Troubleshooting + +**Out of memory errors**: Use `as_iterator=True` to process in chunks instead of loading the entire file. + +**Slow download**: Bulk downloads depend on OECD's file server speed. Try again later if slow. Once downloaded, files are cached. + +**Column names don't match examples**: You're likely comparing bulk downloads (.Stat schema) to API downloads. See [Schema Translation](schema-translation.md). + +**File not found errors**: Older CRS year-specific files use grouped years (e.g., "1995-99"). Check which grouping includes your target year. + +## Next Steps + +- **[Caching & Performance](caching.md)** - Understand how bulk downloads are cached +- **[Schema Translation](schema-translation.md)** - Learn about .Stat vs. API schema differences +- **[Filtering Data](filtering.md)** - Apply filters to bulk downloaded data using pandas diff --git a/docs/docs/caching.md b/docs/docs/caching.md new file mode 100644 index 0000000..a504748 --- /dev/null +++ b/docs/docs/caching.md @@ -0,0 +1,240 @@ +# Caching & Performance + +ODA Reader uses caching to make repeated queries fast and reduce dependency on OECD's servers. This page explains how caching works and how to configure it. + +## How Caching Works + +ODA Reader caches two types of data: + +1. **HTTP responses**: Raw API responses before processing +2. **DataFrames**: Processed pandas DataFrames after schema translation + +Both caches are automatic and transparent - you don't need to change your code to benefit from caching. + +**Example of caching in action**: + +```python +from oda_reader import download_dac1 +import time + +# First call: hits the API (slow) +start = time.time() +data1 = download_dac1(start_year=2022, end_year=2022) +print(f"First call: {time.time() - start:.1f} seconds") + +# Second call: uses cache (instant) +start = time.time() +data2 = download_dac1(start_year=2022, end_year=2022) +print(f"Second call: {time.time() - start:.1f} seconds") +``` + +**Typical output**: +``` +First call: 15.3 seconds +Second call: 0.1 seconds +``` + +Cached queries are ~100x faster. + +## Cache Location + +By default, caches are stored in: +``` +src/oda_reader/.cache/ +``` + +This is inside the package installation directory. + +### Get Current Cache Location + +```python +from oda_reader import get_cache_dir + +location = get_cache_dir() +print(f"Cache directory: {location}") +``` + +### Change Cache Location + +You can set a custom cache location: + +```python +from oda_reader import set_cache_dir + +# Move cache to your project directory +set_cache_dir("/path/to/my/project/oda_cache") +``` + +Or use an environment variable (set before importing oda_reader): + +```bash +export ODA_READER_CACHE_DIR="/path/to/cache" +``` + +```python +import oda_reader +# Cache is now at /path/to/cache +``` + +### Reset to Default Location + +```python +from oda_reader import reset_cache_dir + +reset_cache_dir() +``` + +This reverts to the default location inside the package directory. + +## Managing the Cache + +### Clear All Cached Data + +```python +from oda_reader import clear_cache + +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 + +### Automatic Cache Cleanup + +ODA Reader automatically enforces cache limits: + +- **Max size**: 2.5 GB +- **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 + +This happens automatically - you don't need to do anything. + +## HTTP Caching (Separate from DataFrame Cache) + +ODA Reader also caches raw HTTP responses using `requests-cache`: + +```python +from oda_reader import ( + enable_http_cache, + disable_http_cache, + clear_http_cache, + get_http_cache_info +) +``` + +### Enable/Disable HTTP Cache + +```python +# HTTP cache is enabled by default + +# Disable temporarily +disable_http_cache() + +# Re-enable +enable_http_cache() +``` + +### Clear HTTP Cache Only + +```python +# Clear just HTTP cache (keeps DataFrame cache) +clear_http_cache() +``` + +### Get HTTP Cache Info + +```python +info = get_http_cache_info() +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) + +Both caches speed up repeated queries, but DataFrame cache is faster since it skips parsing. + +## Rate Limiting + +To avoid hitting OECD's API rate limits, ODA Reader automatically pauses between requests. + +**Default rate limit**: 20 calls per 60 seconds + +This is conservative and should prevent rate limit errors. You can customize it: + +```python +from oda_reader import API_RATE_LIMITER + +# More aggressive (use carefully) +API_RATE_LIMITER.max_calls = 30 +API_RATE_LIMITER.period = 60 # seconds + +# More conservative (if you're getting rate limit errors) +API_RATE_LIMITER.max_calls = 10 +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 + +This is transparent - your code just runs slower when rate limit is reached. + +**Example**: If you make 20 calls in 30 seconds, the 21st call waits 30 more seconds before proceeding. + +## Performance Tips + +### Use Bulk Downloads for Large Queries + +If you need large amounts of data, bulk downloads are faster than API calls: + +```python +from oda_reader import bulk_download_crs + +# Much faster than download_crs() for full dataset +crs_full = bulk_download_crs() +``` + +See [Bulk Downloads](bulk-downloads.md) for details. + +### Cache Survives Across Sessions + +Once data is cached, it stays cached between Python sessions: + +```python +# Session 1 +from oda_reader import download_dac1 +data = download_dac1(start_year=2022, end_year=2022) # slow + +# Session 2 (later, even after restarting Python) +from oda_reader import download_dac1 +data = download_dac1(start_year=2022, end_year=2022) # instant +``` + +Cache persists until you clear it or it expires (7 days). + +### Filter Aggressively to Reduce API Load + +Smaller queries are faster and more cache-friendly: + +```python +# Slow: downloads everything +data = download_crs(start_year=2010, end_year=2023) + +# Faster: filter for what you need +data = download_crs( + start_year=2010, + end_year=2023, + filters={"donor": "USA", "sector": "120"} +) +``` diff --git a/docs/docs/changelog.md b/docs/docs/changelog.md new file mode 100644 index 0000000..fb14e84 --- /dev/null +++ b/docs/docs/changelog.md @@ -0,0 +1,41 @@ +# Changelog + +For a complete version history and release notes, see the [CHANGELOG.md](https://github.com/ONEcampaign/oda_reader/blob/main/CHANGELOG.md) file in the repository. + +## Recent Releases + +The changelog includes: +- New features and enhancements +- Bug fixes +- Breaking changes and migration guides +- Deprecation notices + +Visit the [repository changelog](https://github.com/ONEcampaign/oda_reader/blob/main/CHANGELOG.md) for full details. + +## 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 + +## Staying Updated + +To upgrade to the latest version: + +```bash +pip install --upgrade oda-reader +``` + +Or with uv: + +```bash +uv pip install --upgrade oda-reader +``` + +Check your current version: + +```python +import oda_reader +print(oda_reader.__version__) +``` diff --git a/docs/docs/datasets.md b/docs/docs/datasets.md new file mode 100644 index 0000000..dd3d3e6 --- /dev/null +++ b/docs/docs/datasets.md @@ -0,0 +1,238 @@ +# Datasets Overview + +ODA Reader provides access to five datasets covering official development assistance (ODA), other official flows (OOF), and development finance. Each dataset serves different analytical needs. + +## 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, who gives to whom | +| **CRS** | Project-level microdata | Sector analysis, project details, activity-level data | +| **Multisystem** | Multilateral system usage | Analyzing multilateral channels and contributions | +| **AidData** | Chinese development finance | Non-DAC donor analysis, Chinese aid flows | + +## DAC1: Aggregate Flows + +**What it contains**: Total ODA and OOF by donor, aggregated across all recipients and sectors. This is the highest-level view of development assistance. + +**Key dimensions**: +- Donor (bilateral donors and multilateral organizations) +- Measure type (ODA, OOF, grants, loans, etc.) +- Flow type (commitments, disbursements, grant equivalents) +- Price base (current or constant prices) +- Unit measure (USD millions, national currency, etc.) + +**Use when**: +- You need donor-level totals +- Analyzing overall ODA trends over time +- Comparing donor performance +- Working with high-level aggregates + +**Example**: + +```python +from oda_reader import download_dac1 + +# Get all DAC1 data for 2020-2022 +data = download_dac1(start_year=2020, end_year=2022) + +# Filter for ODA disbursements in constant prices +oda_constant = download_dac1( + start_year=2020, + end_year=2022, + filters={ + "measure": "1010", # Net ODA + "flow_type": "1140", # Disbursements + "price_base": "Q" # Constant prices + } +) +``` + +[Read more about filtering →](filtering.md) + +## DAC2a: Bilateral Flows by Recipient + +**What it contains**: Bilateral ODA and OOF flows broken down by both donor and recipient country. Shows who gives to whom. + +**Key dimensions**: +- Donor (bilateral donors) +- Recipient (receiving countries and regions) +- Measure type (bilateral ODA, imputed multilateral, etc.) +- Price base (current or constant) + +**Use when**: +- Analyzing flows to specific recipient countries +- Understanding bilateral relationships +- Studying geographic distribution of aid +- Comparing different donors to the same recipient + +**Example**: + +```python +from oda_reader import download_dac2a + +# Get flows to Sub-Saharan Africa from all donors +africa_flows = download_dac2a( + start_year=2020, + end_year=2022, + filters={"recipient": "289"} # Sub-Saharan Africa (regional code) +) + +# Get flows from Germany to East African countries +germany_eastafrica = download_dac2a( + start_year=2022, + end_year=2022, + filters={ + "donor": "DEU", + "recipient": ["KEN", "TZA", "UGA", "RWA"] + } +) +``` + +## CRS: Creditor Reporting System (Project-Level Microdata) + +**What it contains**: Individual project and activity-level data with detailed information about each development assistance activity. This is the most granular dataset. + +**Key dimensions**: +- Donor +- Recipient +- Sector (purpose codes at various levels of detail) +- Channel (implementing organization type) +- Modality (grant, loan, equity, etc.) +- Flow type +- Microdata flag (True for project-level, False for semi-aggregates) + +**Use when**: +- You need project-level details (descriptions, amounts, sectors) +- Analyzing sector-specific flows +- Understanding implementation channels +- Detailed activity-level analysis + +**Important**: CRS defaults to **microdata** (project-level). For semi-aggregates matching the online Data Explorer view, set `microdata: False` in filters. + +**Example (microdata)**: + +```python +from oda_reader import download_crs + +# Get all health sector projects from Canada +health_projects = download_crs( + start_year=2022, + end_year=2022, + filters={ + "donor": "CAN", + "sector": "120" # Health sector (3-digit code) + } +) + +# Each row is a project with description, amount, dates, etc. +``` + +**Example (semi-aggregates)**: + +```python +# Get semi-aggregated CRS data (matches online Data Explorer) +semi_agg = download_crs( + start_year=2022, + end_year=2022, + filters={ + "donor": "USA", + "recipient": "NGA", + "microdata": False, + "channel": "_T", # Total across all channels + "modality": "_T" # Total across all modalities + } +) +``` + +**Performance note**: The CRS API is slow for large queries. Consider using [bulk downloads](bulk-downloads.md) for full dataset access. + +## Multisystem: Members' Use of the Multilateral System + +**What it contains**: Data on how DAC members use the multilateral aid system, including core contributions to multilateral organizations and earmarked funding. + +**Key dimensions**: +- Donor +- Recipient (multilateral organizations) +- Channel (specific multilateral organizations) +- Flow type (commitments, disbursements) +- Measure type + +**Use when**: +- Analyzing multilateral contributions +- Understanding core vs. earmarked funding +- Studying specific multilateral channels (World Bank, UN agencies, etc.) + +**Example**: + +```python +from oda_reader import download_multisystem + +# Get all multilateral contributions from France +france_multilateral = download_multisystem( + start_year=2020, + end_year=2022, + filters={"donor": "FRA"} +) + +# Get contributions to World Bank IDA +ida_contributions = download_multisystem( + start_year=2020, + end_year=2022, + filters={"channel": "44002"} # IDA +) +``` + +**Performance note**: Like CRS, Multisystem API can be slow. [Bulk download](bulk-downloads.md) is available for the full dataset. + +## AidData: Chinese Development Finance + +**What it contains**: Project-level data on Chinese development finance activities, compiled by AidData. Covers official finance from China that may not be reported to the OECD. + +**Key dimensions**: +- Commitment year +- Recipient country +- Sector +- Project descriptions +- Flow amounts and types + +**Use when**: +- Analyzing Chinese development finance +- Comparing traditional DAC donors with China +- Studying non-DAC donor activities + +**Example**: + +```python +from oda_reader import download_aiddata + +# Get all AidData records for 2015-2020 +chinese_aid = download_aiddata(start_year=2015, end_year=2020) + +# AidData is downloaded as bulk file, filtered by year after download +``` + +**Note**: AidData comes from Excel files, not the OECD API. It uses a different schema than DAC datasets. + +## Discovering Available Filters + +Each dataset has different dimensions you can filter by. Use `get_available_filters()` to see what's available: + +```python +from oda_reader import get_available_filters + +# See available filters for each dataset +dac1_filters = get_available_filters("dac1") +dac2a_filters = get_available_filters("dac2a") +crs_filters = get_available_filters("crs") +multisystem_filters = get_available_filters("multisystem") +``` + +[Learn more about filtering →](filtering.md) + +## Next Steps + +- **[Filtering Data](filtering.md)** - Build complex queries with multiple dimensions +- **[Bulk Downloads](bulk-downloads.md)** - Download full CRS, Multisystem, or AidData efficiently +- **[Schema Translation](schema-translation.md)** - Understand API vs. .Stat schema codes diff --git a/docs/docs/filtering.md b/docs/docs/filtering.md new file mode 100644 index 0000000..8d2156c --- /dev/null +++ b/docs/docs/filtering.md @@ -0,0 +1,289 @@ +# Filtering Data + +All ODA Reader download functions accept a `filters` parameter that lets you query specific subsets of data. This page explains how filtering works across datasets. + +## Basic Filtering Pattern + +Filters use a dictionary where keys are dimension names and values are dimension codes: + +```python +from oda_reader import download_dac1 + +# Filter for a single donor +data = download_dac1( + start_year=2022, + end_year=2022, + filters={"donor": "USA"} +) +``` + +This pattern works across all datasets: DAC1, DAC2a, CRS, and Multisystem. + +## Filtering with Multiple Values + +Pass a list to filter for multiple values of the same dimension: + +```python +# Filter for multiple donors +data = download_dac1( + start_year=2022, + end_year=2022, + filters={"donor": ["USA", "GBR", "FRA"]} +) +``` + +The result includes rows matching any of the values (logical OR). + +## Combining Multiple Dimensions + +Specify multiple dimensions to narrow your query further: + +```python +# Multiple dimensions: donor AND measure type AND price base +data = download_dac1( + start_year=2022, + end_year=2022, + filters={ + "donor": "FRA", + "measure": "11017", # Grant equivalent of loans + "flow_type": "1160", # Net flows + "price_base": "Q" # Constant prices + } +) +``` + +Multiple dimensions work as logical AND - results must match all specified filters. + +## Discovering Available Filters + +Each dataset has different dimensions. Use `get_available_filters()` to see what's available: + +```python +from oda_reader import get_available_filters + +# Get available filters for DAC1 +dac1_filters = get_available_filters("dac1") +``` + +**Output:** +``` +OrderedDict([('donor', typing.Union[str, list, NoneType]), + ('recipient', typing.Union[str, list, NoneType]), + ('flow_type', typing.Union[str, list, NoneType]), + ('measure', typing.Union[str, list, NoneType]), + ('unit_measure', typing.Union[str, list, NoneType]), + ('price_base', typing.Union[str, list, NoneType])]) +``` + +Each key is a dimension name you can use in `filters`. The type hints show you can pass a string, list, or None. + +By default, `get_available_filters()` prints the result. To suppress printing: + +```python +filters = get_available_filters("dac1", quiet=True) +``` + +**For other datasets:** + +```python +# DAC2a filters +dac2a_filters = get_available_filters("dac2a") + +# CRS filters +crs_filters = get_available_filters("crs") + +# Multisystem filters +multisystem_filters = get_available_filters("multisystem") +``` + +## Dataset-Specific Filters + +### DAC1 and DAC2a + +Common dimensions: +- `donor` - Donor country (ISO3 codes like "USA", "GBR", "FRA") +- `recipient` - Recipient country or region (DAC2a only) +- `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 +- `unit_measure` - "USD" for US dollars, "XDC" for national currency + +**Example**: Get net ODA disbursements in constant prices: + +```python +from oda_reader import download_dac1 + +data = download_dac1( + start_year=2020, + end_year=2022, + filters={ + "measure": "1010", # Net ODA + "flow_type": "1140", # Disbursements + "price_base": "Q" # Constant prices + } +) +``` + +### CRS (Creditor Reporting System) + +CRS has additional dimensions: +- `sector` - Purpose codes (5-digit codes like "12220" for basic health) +- `channel` - Implementing organization (government, NGO, multilateral, etc.) +- `modality` - Grant, loan, equity, etc. +- `microdata` - **Important**: `True` (default) for project-level, `False` for semi-aggregates + +**Example**: Get microdata for education sector from Germany: + +```python +from oda_reader import download_crs + +education_projects = download_crs( + start_year=2022, + end_year=2022, + filters={ + "donor": "DEU", + "sector": "110" # Education (3-digit aggregate) + } +) +# Returns individual projects (microdata=True by default) +``` + +### CRS Semi-Aggregates + +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) + +**Example**: Get semi-aggregated data matching Data Explorer: + +```python +# Semi-aggregated totals (matches online Data Explorer) +semi_agg = download_crs( + start_year=2022, + end_year=2022, + filters={ + "donor": "USA", + "recipient": "NGA", + "microdata": False, + "channel": "_T", # Total - required for semi-aggregates + "modality": "_T" # Total - required for semi-aggregates + } +) +``` + +The `_T` suffix means "total" - it aggregates across that dimension to avoid double-counting. + +### Multisystem + +Multisystem tracks multilateral contributions: +- `donor` - Contributing country +- `channel` - Specific multilateral organization (e.g., "44002" for World Bank IDA) +- `flow_type` - Commitments, disbursements +- `measure` - Core contributions, earmarked funds, etc. + +**Example**: Get contributions to UN agencies from Canada: + +```python +from oda_reader import download_multisystem + +un_contributions = download_multisystem( + start_year=2020, + end_year=2022, + filters={ + "donor": "CAN", + "channel": "41000" # UN agencies (aggregate code) + } +) +``` + +## Filter Code Lookup + +Filter values use codes, not human-readable names. To find the right codes: + +1. **Download without filters** and inspect unique values: + +```python +data = download_dac1(start_year=2022, end_year=2022) +print(data['donor'].unique()) # See all donor codes +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 + +**Note**: Codes differ between API schema and .Stat schema. By default, ODA Reader returns .Stat codes. See [Schema Translation](schema-translation.md) for details. + +## Empty Filters + +Pass an empty dictionary or omit `filters` to get all data (subject to year range): + +```python +# These are equivalent - both return all data +data1 = download_dac1(start_year=2022, end_year=2022) +data2 = download_dac1(start_year=2022, end_year=2022, filters={}) +``` + +Be careful with unfiltered CRS queries - they can be very large and slow. + +## Common Filtering Patterns + +### By donor and recipient + +```python +# Bilateral flows from US to Kenya +us_to_kenya = download_dac2a( + start_year=2020, + end_year=2022, + filters={ + "donor": "USA", + "recipient": "KEN" + } +) +``` + +### By sector + +```python +# Health sector activities from all donors +health = download_crs( + start_year=2022, + end_year=2022, + filters={"sector": "120"} # Health (3-digit) +) +``` + +### By multiple criteria + +```python +# French education grants to West Africa in constant prices +french_education = download_crs( + start_year=2020, + end_year=2022, + filters={ + "donor": "FRA", + "recipient": "298", # West Africa (regional code) + "sector": "110", # Education + "modality": "100", # Grants + "price_base": "Q" # Constant prices + } +) +``` + +## Troubleshooting + +**Query returns no data**: Check your filter codes are valid. Try removing filters one by one to identify the issue. + +**Query is slow**: The CRS API is inherently slow for large queries. Consider using [bulk downloads](bulk-downloads.md) instead. + +**Filter not working**: Make sure the dimension name matches exactly (case-sensitive). Use `get_available_filters()` to verify spelling. + +**Unexpected results**: Remember that multiple values in one filter are OR, but multiple filters are AND. Also check if you need semi-aggregates (CRS `microdata: False`). + +## Next Steps + +- **[Bulk Downloads](bulk-downloads.md)** - Download large datasets efficiently +- **[Schema Translation](schema-translation.md)** - Understand filter code differences between schemas +- **[Datasets Overview](datasets.md)** - Learn which dimensions each dataset offers diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md new file mode 100644 index 0000000..9279bdf --- /dev/null +++ b/docs/docs/getting-started.md @@ -0,0 +1,126 @@ +# Getting Started with ODA Reader + +ODA Reader provides simple Python functions to download OECD DAC data. This page walks you through installation and your first queries. + +## Installation + +Install ODA Reader from PyPI using pip: + +```bash +pip install oda-reader +``` + +Or using uv (recommended for faster installs): + +```bash +uv pip install oda-reader +``` + +That's it! ODA Reader and its dependencies (pandas, requests, pyarrow, etc.) are now installed. + +## Your First Query: Download DAC1 Data + +Let's download aggregate ODA data from DAC1 for a single year: + +```python +from oda_reader import download_dac1 + +# Download all available DAC1 data for 2022 +data = download_dac1(start_year=2022, end_year=2022) + +print(f"Downloaded {len(data)} rows") +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 +0 20000 DAC Members 5 Official and private flows 1140 Disbursements, net A Current prices Not applicable 2022 510041.33 6 +1 20000 DAC Members 1010 Official Development Assistance (ODA) 1140 Disbursements, net A Current prices Not applicable 2022 240675.09 6 +2 20000 DAC Members 1015 Bilateral ODA 1140 Disbursements, net A Current prices Not applicable 2022 190247.3 6 +``` + +The function returns a pandas DataFrame with columns for donor, measure type, flow type, amount, and more. By default, ODA Reader: + +- Preprocesses column names to be machine-readable +- Converts to OECD.Stat schema codes for compatibility +- Caches results for fast repeated queries + +## Filtering Your Query + +You can filter downloads to specific dimensions. Let's get data for just the USA and UK: + +```python +from oda_reader import download_dac1 + +# Filter for specific donors +data = download_dac1( + start_year=2022, + end_year=2022, + filters={"donor": ["USA", "GBR"]} +) + +print(f"Downloaded {len(data)} rows for USA and GBR") +print(data['donor_name'].unique()) +``` + +**Output:** +``` +Downloaded 1851 rows for USA and GBR +['United States', 'United Kingdom'] +``` + +Filters use a dictionary where keys are dimension names and values are codes (single value or list). This pattern works across all datasets. + +## Download a Different Dataset: DAC2a + +DAC2a contains bilateral flows by recipient. Let's download data for specific recipients: + +```python +from oda_reader import download_dac2a + +# Download DAC2a data for Nigeria and Kenya +data = download_dac2a( + start_year=2022, + end_year=2022, + filters={"recipient": ["NGA", "KEN"]} +) + +print(f"Downloaded {len(data)} rows") +print(f"Recipients: {sorted(data['recipient_name'].unique())}") +``` + +**Output:** +``` +Downloaded 3724 rows +Recipients: ['Kenya', 'Nigeria'] +``` + +DAC2a includes recipient countries as a dimension, making it ideal for analyzing who receives aid from whom. + +## What Just Happened? + +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 + +## Next Steps + +Now that you've downloaded your first datasets, explore: + +- **[Datasets Overview](datasets.md)** - Learn about all 5 available datasets and when to use each +- **[Filtering Data](filtering.md)** - Discover available filters and build complex queries +- **[Bulk Downloads](bulk-downloads.md)** - Download full datasets efficiently for large-scale analysis +- **[Caching & Performance](caching.md)** - Manage cache and configure rate limiting + +## Troubleshooting + +**Query is slow**: First-time queries can take 10-30 seconds as ODA Reader fetches from OECD's API. Subsequent identical queries are instant due to caching. + +**Rate limit errors**: By default, ODA Reader limits to 20 requests per 60 seconds. This should prevent rate limit errors. If you see them, your cache might have been cleared. Wait a minute and retry. + +**Import errors**: Make sure you installed with dependencies: `pip install oda-reader` (not just `oda_reader`). diff --git a/docs/docs/index.md b/docs/docs/index.md new file mode 100644 index 0000000..bb8ccd3 --- /dev/null +++ b/docs/docs/index.md @@ -0,0 +1,42 @@ +# ODA Reader + +**Programmatic access to OECD DAC data without the headaches** + +Working with OECD Development Assistance Committee (DAC) data is frustrating. You need to navigate multiple datasets (DAC1, DAC2a, CRS), understand complex SDMX API syntax, manage rate limits, and reconcile different schema versions. The OECD doesn't provide any first-party Python library to help. + +Worse, the OECD has a habit of introducing undocumented schema changes, breaking link URLs, and making format changes without notice. What works today might break tomorrow, making it extremely difficult to build robust data pipelines for research and analysis. + +ODA Reader eliminates these headaches. It provides a unified Python interface that handles complexity for you: automatic version fallbacks when schemas change, consistent APIs across datasets, smart caching to reduce dependency on flaky endpoints, and schema translation between API and legacy formats. + +**Key features**: + +- **Access 5+ datasets** through simple functions: DAC1, DAC2a, CRS, Multisystem, AidData +- **Apply filters easily**: `filters={"donor": "USA", "recipient": "NGA"}` works across datasets +- **Bulk download large files** with memory-efficient streaming for the full CRS (1GB+) +- **Automatic rate limiting** and caching to work within API constraints +- **Schema translation** between Data Explorer API and OECD.Stat formats +- **Version fallback** automatically retries with older schema versions when OECD makes breaking changes + +**Built for researchers, analysts, and developers** who need reliable, programmatic access to ODA data without fighting infrastructure. + +## Quick Example + +```python +from oda_reader import download_dac1 + +# Download aggregate ODA flows from 2020-2022 +data = download_dac1(start_year=2020, end_year=2022) + +# Filter for specific donors +us_uk_data = download_dac1( + start_year=2020, + end_year=2022, + filters={"donor": ["USA", "GBR"]} +) +``` + +## Next Steps + +- [Why ODA Reader](why-oda-reader.md) - Understand the rationale and compare to alternatives +- [Getting Started](getting-started.md) - Install and run your first queries in 5 minutes +- [Datasets Overview](datasets.md) - Learn about the 5 available datasets diff --git a/docs/docs/schema-translation.md b/docs/docs/schema-translation.md new file mode 100644 index 0000000..f335ca5 --- /dev/null +++ b/docs/docs/schema-translation.md @@ -0,0 +1,332 @@ +# Schema Translation + +OECD DAC data exists in two schema formats: the modern Data Explorer API schema and the legacy OECD.Stat schema. ODA Reader can translate between them. + +## The Two Schemas + +### Data Explorer API Schema (New) + +The current OECD Data Explorer uses a new schema: +- Column names: `DONOR`, `RECIPIENT`, `MEASURE`, etc. (all caps) +- Dimension codes: Modern conventions (e.g., donor codes) +- Used by: API downloads (`download_dac1()`, `download_crs()`, etc.) + +### OECD.Stat Schema (Legacy) + +The older OECD.Stat system uses a different schema: +- Column names: `DonorCode`, `RecipientCode`, `Measure`, etc. (mixed case) +- Dimension codes: Legacy conventions, sometimes different from API codes +- Used by: Bulk download files, historical .Stat exports + +**Why two schemas?** OECD transitioned from .Stat to Data Explorer but kept bulk files in the old format. Both are still in use. + +## Translation Parameters + +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 + +**Example without preprocessing**: + +```python +from oda_reader import download_dac1 + +# Raw API response +data_raw = download_dac1( + start_year=2022, + end_year=2022, + pre_process=False, + dotstat_codes=False +) + +print(data_raw.columns) +# Output: ['DONOR', 'RECIPIENT', 'MEASURE', 'AMOUNT_TYPE', 'FLOW_CODE', ...] +# (exact API column names, all caps) +``` + +**Example with preprocessing**: + +```python +# Preprocessed (default) +data_clean = download_dac1( + start_year=2022, + end_year=2022, + pre_process=True, + dotstat_codes=False +) + +print(data_clean.columns) +# Output: ['donor', 'recipient', 'measure', 'amount_type', 'flow_code', ...] +# (clean, consistent naming) +``` + +### `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 + +**Example without code translation**: + +```python +# API codes only +data_api = download_dac1( + start_year=2022, + end_year=2022, + pre_process=True, + dotstat_codes=False +) + +print(data_api['donor'].unique()[:5]) +# Output: ['1', '2', '3', '4', '5'] +# (numeric API codes) +``` + +**Example with code translation**: + +```python +# Translated to .Stat codes (default) +data_stat = download_dac1( + start_year=2022, + end_year=2022, + pre_process=True, + dotstat_codes=True +) + +print(data_stat['donor'].unique()[:5]) +# Output: ['AUS', 'AUT', 'BEL', 'CAN', 'CHE'] +# (ISO3 donor codes, matches .Stat format) +``` + +## Three Modes Explained + +### Mode 1: Default (Recommended) + +```python +data = download_dac1(start_year=2022, end_year=2022) +# Equivalent to: +# pre_process=True, dotstat_codes=True +``` + +**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 +- Codes are human-readable (ISO3 country codes) +- Compatible with bulk download files + +### Mode 2: Raw API Response + +```python +data = download_dac1( + start_year=2022, + end_year=2022, + pre_process=False, + dotstat_codes=False +) +``` + +**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) +- Codes are not human-readable + +### Mode 3: Preprocessed with API Codes + +```python +data = download_dac1( + start_year=2022, + end_year=2022, + pre_process=True, + dotstat_codes=False +) +``` + +**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 + +## Code Translation Examples + +### Donor Codes + +| API Code | .Stat Code | Country | +|----------|------------|---------| +| `1` | `AUS` | Australia | +| `2` | `AUT` | Austria | +| `12` | `USA` | United States | +| `301` | `GBR` | United Kingdom | + +### Measure Codes (DAC1) + +| API Code | .Stat Code | Description | +|----------|------------|-------------| +| `100` | `1010` | Net ODA | +| `106` | `1011` | ODA Grants | +| `11017` | `11017` | Grant equiv. of loans | + +(Note: Some codes are the same across schemas) + +### Flow Type Codes + +| API Code | .Stat Code | Description | +|----------|------------|-------------| +| `A` | `1140` | Disbursements | +| `C` | `1110` | Commitments | +| `D` | `1160` | Net flows | + +Translation mappings are maintained in `src/oda_reader/schemas/mappings/` as JSON files. + +## Bulk Download Schema + +**Important**: Bulk downloads (CRS, Multisystem, AidData) **always use .Stat schema**. + +```python +from oda_reader import bulk_download_crs + +# This always returns .Stat schema +crs_bulk = bulk_download_crs() + +print(crs_bulk.columns) +# Output: ['DonorCode', 'RecipientCode', 'SectorCode', ...] +# (mixed case, .Stat conventions) +``` + +There are no `pre_process` or `dotstat_codes` parameters for bulk downloads - the files are already in .Stat format. + +**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: + +```python +# API download with .Stat codes +api_data = download_crs( + start_year=2023, + end_year=2023, + pre_process=True, + dotstat_codes=True +) + +# Bulk download (always .Stat) +bulk_data = bulk_download_crs() + +# Column names differ slightly: +print(api_data.columns[:5]) # ['donor', 'recipient', 'year', 'sector', ...] +print(bulk_data.columns[:5]) # ['DonorCode', 'RecipientCode', 'Year', 'SectorCode', ...] + +# But codes are the same (both use .Stat codes): +print(api_data['donor'].unique()[:3]) # ['USA', 'GBR', 'DEU'] +print(bulk_data['DonorCode'].unique()[:3]) # ['USA', 'GBR', 'DEU'] +``` + +You can harmonize column names with pandas: + +```python +# Rename bulk columns to match API preprocessing +bulk_data = bulk_data.rename(columns={ + 'DonorCode': 'donor', + 'RecipientCode': 'recipient', + # ... etc +}) +``` + +## When to Use Which Mode + +**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) + +**Use raw mode (pre_process=False, dotstat_codes=False)**: +- ✅ Debugging API issues +- ✅ Understanding API response structure +- ❌ Not recommended for analysis + +**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 + +## Finding Code Mappings + +Code translation mappings are defined in: +``` +src/oda_reader/schemas/mappings/ +├── dac1_mapping.json +├── dac2_mapping.json +├── crs_mapping.json +└── multisystem_mapping.json +``` + +Each file maps API codes to .Stat codes for that dataset's dimensions. + +**Example from `dac1_mapping.json`**: + +```json +{ + "DONOR": { + "keep": true, + "name": "donor", + "type": "string[pyarrow]", + "mapping": { + "1": "AUS", + "2": "AUT", + "12": "USA" + } + } +} +``` + +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) + +## Next Steps + +- **[Bulk Downloads](bulk-downloads.md)** - Understand bulk download schema +- **[Filtering Data](filtering.md)** - Use the right codes for filtering +- **[Advanced Topics](advanced.md)** - Custom schema handling diff --git a/docs/docs/why-oda-reader.md b/docs/docs/why-oda-reader.md new file mode 100644 index 0000000..35bedc0 --- /dev/null +++ b/docs/docs/why-oda-reader.md @@ -0,0 +1,124 @@ +# Why ODA Reader? + +This page explains why ODA Reader exists, how it compares to alternatives, and when you might want to use it (or not). + +## 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: + +**No official Python library**: The OECD doesn't provide any first-party Python tools for accessing DAC data. You're on your own to figure out the SDMX API, construct queries, and parse responses. + +**Undocumented breaking changes**: The OECD regularly introduces schema changes without documentation or warning. A dataflow version that worked last month might return 404 errors today. Link URLs change, breaking saved bookmarks and automated downloads. + +**Inconsistent formats**: Different datasets use different schemas. The new Data Explorer API uses one set of dimension codes, while legacy .Stat files and bulk downloads use another. Reconciling these takes significant effort. + +**Complex API syntax**: SDMX queries require precise filter strings where dimension order matters and syntax varies between API versions. One wrong character breaks the entire query. + +## Alternatives and Comparisons + +### Direct SDMX API Usage + +**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 +- Schema changes break queries without warning +- No automatic retries or fallback mechanisms + +**What ODA Reader provides**: Automatic version fallback, rate limiting built-in, consistent function interface, handles schema changes gracefully. + +### Manual Downloads from OECD.Stat + +**Approach**: Download CSV or Excel files from OECD.Stat portal manually. + +**Challenges**: +- No automation - manual clicking and downloading +- Portal URLs change, bookmarks break +- File format inconsistencies between download dates +- No programmatic filtering or querying +- Time-consuming for iterative analysis + +**What ODA Reader provides**: Programmatic bulk downloads with consistent interfaces, automatic format handling, filterable queries, easy iteration. + +### Using Generic SDMX Libraries + +**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 +- No automatic handling of OECD's breaking changes +- Steeper learning curve for SDMX concepts + +**What ODA Reader provides**: DAC-specific functions (`download_dac1`, `download_crs`), automatic schema translation, dataset-specific documentation, simpler API for common tasks. + +## Design Decisions + +### 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 + +ODA Reader provides both because different analysis workflows need different approaches. + +### 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 +- Reproducibility - cached data doesn't change + +You can disable or clear caching when you need fresh data. + +### How Version Fallback Works + +When OECD changes a dataflow schema version, ODA Reader: +1. Tries the configured version (e.g., `1.0`) +2. If 404 error, automatically retries with `0.9` +3. Continues decrementing (0.8, 0.7, 0.6) up to 5 attempts +4. Returns data from first successful version + +This means your code keeps working even when OECD makes breaking changes. + +## Limitations and When Not to Use ODA Reader + +**Be honest about limitations:** + +❌ **Not for real-time data**: Caching introduces delays. If you need the absolute latest data published in the last hour, you'll need to clear cache or use the OECD portal directly. + +❌ **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. + +❌ **Only covers DAC data**: ODA Reader focuses exclusively on Development Assistance Committee datasets. For other OECD data (economic indicators, education statistics, etc.), you'll need different tools. + +❌ **Bulk downloads limited**: Only CRS, Multisystem, and AidData have bulk download options. For other datasets, you must use the API. + +❌ **Dependent on OECD availability**: While caching helps, initial downloads still depend on OECD's servers being available and responsive. + +## When to Use ODA Reader + +✅ You're doing research or analysis that requires ODA/OOF data + +✅ You need programmatic, reproducible access to multiple datasets + +✅ You're building data pipelines that need to be robust to OECD's changes + +✅ You want to avoid manually managing API rate limits and caching + +✅ You need to work with both API and bulk download formats + +✅ You're comfortable with Python and pandas + +## Next Steps + +Ready to try it? Head to [Getting Started](getting-started.md) to install ODA Reader and run your first queries. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml new file mode 100644 index 0000000..41d3365 --- /dev/null +++ b/docs/mkdocs.yml @@ -0,0 +1,61 @@ +site_name: ODA Reader +site_description: Python package for accessing OECD DAC data +site_author: The ONE Campaign + +theme: + name: material + palette: + - scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - scheme: slate + primary: indigo + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + - navigation.tabs + - navigation.sections + - navigation.expand + - search.suggest + - search.highlight + - content.code.copy + +nav: + - Home: index.md + - Why ODA Reader: why-oda-reader.md + - Getting Started: getting-started.md + - Datasets Overview: datasets.md + - Filtering Data: filtering.md + - Bulk Downloads: bulk-downloads.md + - Caching & Performance: caching.md + - Schema Translation: schema-translation.md + - Advanced Topics: advanced.md + - API Reference: api-reference.md + - Changelog: changelog.md + +markdown_extensions: + - admonition + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.superfences + - pymdownx.details + - pymdownx.snippets + - pymdownx.inlinehilite + - attr_list + - md_in_html + +plugins: + - search + - mkdocstrings: + handlers: + python: + options: + show_source: false + show_root_heading: true + show_root_full_path: false + members_order: source diff --git a/pyproject.toml b/pyproject.toml index fa34e9f..50a7800 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,5 +26,109 @@ build-backend = "uv_build" [dependency-groups] dev = [ + "pre-commit>=4.0.0", "ruff>=0.14.0", ] +docs = [ + "mkdocs>=1.5.0", + "mkdocs-material>=9.5.0", + "mkdocstrings[python]>=0.24.0", +] +test = [ + "pytest>=8.0", + "pytest-mock>=3.12", + "pytest-cov>=4.1", + "pytest-xdist>=3.5", +] + +[tool.ruff] +# Set the maximum line length +line-length = 88 + +# Target Python 3.10+ +target-version = "py310" + +# Exclude common directories +exclude = [ + ".git", + ".venv", + "venv", + "__pycache__", + "build", + "dist", + "*.egg-info", + ".pytest_cache", + "htmlcov", + ".cache", +] + +[tool.ruff.lint] +# Enable recommended rules plus extras +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "N", # pep8-naming + "UP", # pyupgrade + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "SIM", # flake8-simplify + "PL", # pylint + "RUF", # ruff-specific rules +] + +# Ignore specific rules that might be too strict +ignore = [ + "E501", # Line too long (handled by formatter) + "PLR0913", # Too many arguments + "PLR2004", # Magic value used in comparison + "RUF022", # Unsorted __all__ (intentional grouping by category) + "RUF013", # Implicit Optional (cleaner syntax for defaults) +] + +# Allow autofix for all enabled rules +fixable = ["ALL"] +unfixable = [] + +[tool.ruff.lint.per-file-ignores] +# Allow longer lines in tests and examples +"tests/**/*.py" = ["E501"] +"docs/examples/**/*.py" = ["E501"] +# Allow global variable usage in cache and common modules (architectural decision) +"src/oda_reader/_cache/*.py" = ["PLW0603"] +"src/oda_reader/common.py" = ["PLW0603"] +# Allow try-except without from (external libraries may not support cause chaining) +"src/oda_reader/download/download_tools.py" = ["B904"] +# Allow camelcase import in tools (QueryBuilder is the standard name) +"src/oda_reader/tools.py" = ["N813"] + +[tool.ruff.format] +# Use double quotes for strings +quote-style = "double" + +# Use spaces for indentation +indent-style = "space" + +# Like Black, respect magic trailing commas +skip-magic-trailing-comma = false + +# Like Black, automatically detect line endings +line-ending = "auto" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +markers = [ + "unit: Fast unit tests (no external dependencies)", + "integration: Tests that call real OECD API", + "slow: Long-running tests (bulk downloads)", + "cache: Tests that verify cache behavior", +] +addopts = [ + "-v", + "--strict-markers", + "-m", "not integration", +] diff --git a/src/oda_reader/__init__.py b/src/oda_reader/__init__.py index 6d785f5..c20975a 100644 --- a/src/oda_reader/__init__.py +++ b/src/oda_reader/__init__.py @@ -4,36 +4,34 @@ """ # Core data download functions -from oda_reader.download.query_builder import QueryBuilder -from oda_reader.dac1 import download_dac1 -from oda_reader.dac2a import download_dac2a -from oda_reader.multisystem import download_multisystem, bulk_download_multisystem -from oda_reader.crs import download_crs, bulk_download_crs, download_crs_file -from oda_reader.aiddata import download_aiddata -from oda_reader.tools import get_available_filters - # Cache management (new system) from oda_reader._cache import ( - get_cache_dir, - set_cache_dir, - reset_cache_dir, - dataframe_cache, bulk_cache_manager, + cache_dir, # Deprecated alias + clear_cache, + dataframe_cache, + disable_cache, # Legacy functions (for backward compatibility) enable_cache, - disable_cache, - clear_cache, enforce_cache_limits, - cache_dir, # Deprecated alias + get_cache_dir, + reset_cache_dir, + set_cache_dir, ) +from oda_reader.aiddata import download_aiddata from oda_reader.common import ( API_RATE_LIMITER, - enable_http_cache, - disable_http_cache, clear_http_cache, + disable_http_cache, + enable_http_cache, get_http_cache_info, ) - +from oda_reader.crs import bulk_download_crs, download_crs, download_crs_file +from oda_reader.dac1 import download_dac1 +from oda_reader.dac2a import download_dac2a +from oda_reader.download.query_builder import QueryBuilder +from oda_reader.multisystem import bulk_download_multisystem, download_multisystem +from oda_reader.tools import get_available_filters __all__ = [ # Data download diff --git a/src/oda_reader/_cache/__init__.py b/src/oda_reader/_cache/__init__.py index 41b04ec..d77a33f 100644 --- a/src/oda_reader/_cache/__init__.py +++ b/src/oda_reader/_cache/__init__.py @@ -14,19 +14,12 @@ # Configuration from oda_reader._cache.config import ( - get_cache_dir, - set_cache_dir, - reset_cache_dir, - get_http_cache_path, get_bulk_cache_dir, + get_cache_dir, get_dataframe_cache_dir, -) - -# Bulk file cache manager -from oda_reader._cache.manager import ( - CacheEntry, - CacheManager, - bulk_cache_manager, + get_http_cache_path, + reset_cache_dir, + set_cache_dir, ) # DataFrame cache @@ -37,18 +30,27 @@ # Legacy functions (backward compatibility) from oda_reader._cache.legacy import ( - memory, + CACHE_MAX_AGE_HOURS, + CACHE_MAX_SIZE_MB, cache_dir, - set_cache_dir as legacy_set_cache_dir, - get_cache_size_mb, + cache_info, clear_cache, clear_old_cache_entries, - enforce_cache_limits, - cache_info, disable_cache, enable_cache, - CACHE_MAX_SIZE_MB, - CACHE_MAX_AGE_HOURS, + enforce_cache_limits, + get_cache_size_mb, + memory, +) +from oda_reader._cache.legacy import ( + set_cache_dir as legacy_set_cache_dir, +) + +# Bulk file cache manager +from oda_reader._cache.manager import ( + CacheEntry, + CacheManager, + bulk_cache_manager, ) __all__ = [ diff --git a/src/oda_reader/_cache/config.py b/src/oda_reader/_cache/config.py index 51974ed..83e8e1d 100644 --- a/src/oda_reader/_cache/config.py +++ b/src/oda_reader/_cache/config.py @@ -6,7 +6,6 @@ import os from pathlib import Path -from typing import Optional from platformdirs import user_cache_dir @@ -15,7 +14,7 @@ __version__ = "1.3.0" # Global override for cache directory (set via set_cache_dir) -_CACHE_DIR_OVERRIDE: Optional[Path] = None +_CACHE_DIR_OVERRIDE: Path | None = None def get_cache_dir() -> Path: diff --git a/src/oda_reader/_cache/dataframe.py b/src/oda_reader/_cache/dataframe.py index 1fa9c57..1136551 100644 --- a/src/oda_reader/_cache/dataframe.py +++ b/src/oda_reader/_cache/dataframe.py @@ -16,7 +16,7 @@ import json import logging from pathlib import Path -from typing import Any, Optional +from typing import Any import pandas as pd @@ -77,7 +77,7 @@ class DataFrameCache: Uses parquet files for efficient storage and fast loading. """ - def __init__(self, cache_dir: Optional[Path] = None): + def __init__(self, cache_dir: Path | None = None): """Initialize DataFrame cache. Args: @@ -95,7 +95,7 @@ def get( pre_process: bool, dotstat_codes: bool, **kwargs, - ) -> Optional[pd.DataFrame]: + ) -> pd.DataFrame | None: """Get a cached DataFrame if it exists. Args: @@ -206,7 +206,7 @@ def disable(self) -> None: # Global singleton -_DATAFRAME_CACHE: Optional[DataFrameCache] = None +_DATAFRAME_CACHE: DataFrameCache | None = None def dataframe_cache() -> DataFrameCache: diff --git a/src/oda_reader/_cache/legacy.py b/src/oda_reader/_cache/legacy.py index a8b49ca..361e383 100644 --- a/src/oda_reader/_cache/legacy.py +++ b/src/oda_reader/_cache/legacy.py @@ -12,7 +12,6 @@ import shutil import time from pathlib import Path -from typing import Optional from oda_reader._cache.config import get_cache_dir @@ -24,7 +23,7 @@ _has_logged_cache_message = False # Legacy joblib support (deprecated) -_JOBLIB_MEMORY: Optional[object] = None +_JOBLIB_MEMORY: object | None = None def memory(): @@ -196,8 +195,8 @@ def disable_cache() -> None: This disables HTTP caching and DataFrame caching. """ - from oda_reader.common import disable_http_cache from oda_reader._cache.dataframe import dataframe_cache + from oda_reader.common import disable_http_cache disable_http_cache() dataframe_cache().disable() @@ -209,8 +208,8 @@ def enable_cache() -> None: This enables HTTP caching and DataFrame caching. """ - from oda_reader.common import enable_http_cache from oda_reader._cache.dataframe import dataframe_cache + from oda_reader.common import enable_http_cache enable_http_cache() dataframe_cache().enable() diff --git a/src/oda_reader/_cache/manager.py b/src/oda_reader/_cache/manager.py index 475b1da..9fc762d 100644 --- a/src/oda_reader/_cache/manager.py +++ b/src/oda_reader/_cache/manager.py @@ -7,10 +7,11 @@ import json import logging import os +from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta, timezone from pathlib import Path -from typing import Any, Callable, Optional +from typing import Any from filelock import FileLock @@ -38,7 +39,7 @@ class CacheEntry: filename: str fetcher: Callable[[Path], None] ttl_days: int = 30 - version: Optional[str] = None + version: str | None = None class CacheManager: @@ -58,7 +59,7 @@ class CacheManager: └── *.parquet # Cached datasets """ - def __init__(self, base_dir: Optional[Path] = None): + def __init__(self, base_dir: Path | None = None): """Initialize cache manager. Args: @@ -124,7 +125,7 @@ def ensure(self, entry: CacheEntry, refresh: bool = False) -> Path: return path - def clear(self, key: Optional[str] = None) -> None: + def clear(self, key: str | None = None) -> None: """Clear cached data. Args: @@ -144,15 +145,14 @@ def clear(self, key: Optional[str] = None) -> None: file_path.unlink(missing_ok=True) manifest.clear() logger.info("Cleared all bulk cache entries") + # Clear specific entry + elif key in manifest: + file_path = self.base_dir / manifest[key]["filename"] + file_path.unlink(missing_ok=True) + del manifest[key] + logger.info(f"Cleared cache entry: {key}") else: - # Clear specific entry - if key in manifest: - file_path = self.base_dir / manifest[key]["filename"] - file_path.unlink(missing_ok=True) - del manifest[key] - logger.info(f"Cleared cache entry: {key}") - else: - logger.warning(f"Cache key not found: {key}") + logger.warning(f"Cache key not found: {key}") self._save_manifest(manifest) @@ -269,7 +269,7 @@ def _save_manifest(self, manifest: dict) -> None: # Global singleton -_BULK_CACHE_MANAGER: Optional[CacheManager] = None +_BULK_CACHE_MANAGER: CacheManager | None = None def bulk_cache_manager() -> CacheManager: diff --git a/src/oda_reader/aiddata.py b/src/oda_reader/aiddata.py index 141f67f..394e835 100644 --- a/src/oda_reader/aiddata.py +++ b/src/oda_reader/aiddata.py @@ -1,14 +1,15 @@ -import pandas as pd from pathlib import Path +import pandas as pd + from oda_reader._cache import cache_info from oda_reader.download.download_tools import ( bulk_download_aiddata, ) from oda_reader.schemas.schema_tools import ( - read_schema_translation, convert_dtypes, preprocess, + read_schema_translation, ) diff --git a/src/oda_reader/common.py b/src/oda_reader/common.py index 3a65524..ddcd658 100644 --- a/src/oda_reader/common.py +++ b/src/oda_reader/common.py @@ -1,11 +1,10 @@ import logging import re +import time +from collections import deque from copy import deepcopy from io import StringIO from pathlib import Path -import time -from collections import deque -from typing import Optional import pandas as pd import requests @@ -22,7 +21,7 @@ # Global HTTP cache session (initialized lazily) -_HTTP_SESSION: Optional[requests_cache.CachedSession] = None +_HTTP_SESSION: requests_cache.CachedSession | None = None _CACHE_ENABLED = True @@ -142,7 +141,7 @@ class ImporterPaths: cache = scripts / ".cache" -def text_to_stringIO(response_text: str) -> StringIO: +def text_to_stringio(response_text: str) -> StringIO: """Convert the content of a response to bytes. Args: @@ -242,10 +241,7 @@ def get_data_from_api(url: str, compressed: bool = True, retries: int = 0) -> st str: The response text from the API. """ # Set the headers with gzip encoding (if required) - if compressed: - headers = {"Accept-Encoding": "gzip"} - else: - headers = {} + headers = {"Accept-Encoding": "gzip"} if compressed else {} # Fetch the data with headers status_code, response, from_cache = _get_response_text(url, headers=headers) @@ -300,7 +296,7 @@ def api_response_to_df( response = get_data_from_api(url=url, compressed=compressed) # Convert the content to stringIO - data = text_to_stringIO(response) + data = text_to_stringio(response) # Return the data as a DataFrame try: diff --git a/src/oda_reader/crs.py b/src/oda_reader/crs.py index 87210a6..9e9a2d2 100644 --- a/src/oda_reader/crs.py +++ b/src/oda_reader/crs.py @@ -1,26 +1,25 @@ +import typing from pathlib import Path import pandas as pd -import typing from oda_reader._cache import cache_info from oda_reader.common import logger from oda_reader.download.download_tools import ( - get_bulk_file_id, - bulk_download_parquet, CRS_FLOW_URL, + bulk_download_parquet, download, + get_bulk_file_id, ) DATAFLOW_ID: str = "DSD_CRS@DF_CRS" DATAFLOW_ID_GE: str = "DSD_GREQ@DF_CRS_GREQ" DATAFLOW_VERSION: str = "1.6" -""" -{donor}.{recipient}.{sector}.{measure}.{channel}. - {modality}.{flow_type}.{price_base}.{md_dim}.{md_id}.{unit_measure}. - {time_period} -""" +# CRS filter structure: +# {donor}.{recipient}.{sector}.{measure}.{channel}. +# {modality}.{flow_type}.{price_base}.{md_dim}.{md_id}.{unit_measure}. +# {time_period} def get_full_crs_parquet_id(): @@ -50,10 +49,8 @@ def download_crs_file( Args: year: The year of CRS data to download. - save_to_path: The path to save the file to. Optional. If not provided, a - DataFrame is returned. - as_iterator: If ``True`` yields ``DataFrame`` chunks instead of a single - ``DataFrame``. + save_to_path: The path to save the file to. Optional. If not provided, a DataFrame is returned. + as_iterator: If ``True`` yields ``DataFrame`` chunks instead of a single ``DataFrame``. Returns: pd.DataFrame | Iterator[pd.DataFrame] | None @@ -82,11 +79,9 @@ def bulk_download_crs( provided, the function will return a DataFrame. Args: - save_to_path: The path to save the file to. Optional. If not provided, a - DataFrame is returned. + save_to_path: The path to save the file to. Optional. If not provided, a DataFrame is returned. reduced_version: Whether to download the reduced version of the CRS data. - as_iterator: If ``True`` yields ``DataFrame`` chunks instead of a single - ``DataFrame``. + as_iterator: If ``True`` yields ``DataFrame`` chunks instead of a single ``DataFrame``. Returns: pd.DataFrame | Iterator[pd.DataFrame] | None @@ -122,11 +117,9 @@ def download_crs( start_year (int): The start year of the data to download. Optional end_year (int): The end year of the data to download. Optional filters (dict): Optional filters to pass to the download. - pre_process (bool): Whether to preprocess the data. Defaults to True. - Preprocessing makes it comply with the .stat schema. + pre_process (bool): Whether to preprocess the data. Defaults to True. Preprocessing makes it comply with the .stat schema. dotstat_codes (bool): Whether to convert the donor codes to the .stat schema. - as_grant_equivalent (bool): Whether to download the grant equivalent data - instead of flows. + as_grant_equivalent (bool): Whether to download the grant equivalent data instead of flows. dataflow_version (str): The version of the dataflow to download. Returns: diff --git a/src/oda_reader/dac1.py b/src/oda_reader/dac1.py index 1b0c6f9..95cda2d 100644 --- a/src/oda_reader/dac1.py +++ b/src/oda_reader/dac1.py @@ -23,8 +23,7 @@ def download_dac1( start_year (int): The start year of the data to download. Optional end_year (int): The end year of the data to download. Optional filters (dict): Optional filters to pass to the download. - pre_process (bool): Whether to preprocess the data. Defaults to True. - Preprocessing makes it comply with the .stat schema. + pre_process (bool): Whether to preprocess the data. Defaults to True. Preprocessing makes it comply with the .stat schema. dotstat_codes (bool): Whether to convert the donor codes to the .stat schema. dataflow_version (str): The version of the dataflow. Optional diff --git a/src/oda_reader/dac2a.py b/src/oda_reader/dac2a.py index baf719e..f3367f9 100644 --- a/src/oda_reader/dac2a.py +++ b/src/oda_reader/dac2a.py @@ -24,8 +24,7 @@ def download_dac2a( start_year (int): The start year of the data to download. Optional end_year (int): The end year of the data to download. Optional filters (dict): Optional filters to pass to the download. - pre_process (bool): Whether to preprocess the data. Defaults to True. - Preprocessing makes it comply with the .stat schema. + pre_process (bool): Whether to preprocess the data. Defaults to True. Preprocessing makes it comply with the .stat schema. dotstat_codes (bool): Whether to convert the donor codes to the .stat schema. dataflow_version (str): The version of the data. Optional diff --git a/src/oda_reader/download/download_tools.py b/src/oda_reader/download/download_tools.py index 4d9cf7e..3cbf10e 100644 --- a/src/oda_reader/download/download_tools.py +++ b/src/oda_reader/download/download_tools.py @@ -1,25 +1,25 @@ +import hashlib import io import os import re import shutil import tempfile +import typing import zipfile from pathlib import Path -import hashlib import pandas as pd -import requests -import typing import pyarrow.parquet as pq +import requests from oda_reader._cache.config import get_bulk_cache_dir from oda_reader._cache.dataframe import dataframe_cache from oda_reader.common import ( + API_RATE_LIMITER, + _get_response_content, + _get_response_text, api_response_to_df, logger, - _get_response_text, - _get_response_content, - API_RATE_LIMITER, ) from oda_reader.download.query_builder import QueryBuilder from oda_reader.schemas.crs_translation import convert_crs_to_dotstat_codes @@ -29,9 +29,9 @@ convert_multisystem_to_dotstat_codes, ) from oda_reader.schemas.schema_tools import ( - read_schema_translation, get_dtypes, preprocess, + read_schema_translation, ) BULK_DOWNLOAD_URL = "https://stats.oecd.org/wbos/fileview2.aspx?IDFile=" @@ -51,7 +51,7 @@ def _open_zip(response_content: bytes | Path) -> zipfile.ZipFile: """Open a zip file from bytes or a file path.""" - if isinstance(response_content, (bytes, bytearray)): + if isinstance(response_content, bytes | bytearray): return zipfile.ZipFile(io.BytesIO(response_content)) return zipfile.ZipFile(response_content) @@ -170,9 +170,8 @@ def download( df = preprocess(df=df, schema_translation=schema_translation) if dotstat_codes: df = convert_func(df) - else: - if dotstat_codes: - raise ValueError("Cannot convert to dotstat codes without preprocessing.") + elif dotstat_codes: + raise ValueError("Cannot convert to dotstat codes without preprocessing.") # Cache the processed DataFrame df_cache.set( diff --git a/src/oda_reader/download/query_builder.py b/src/oda_reader/download/query_builder.py index 7dc1432..96f8674 100644 --- a/src/oda_reader/download/query_builder.py +++ b/src/oda_reader/download/query_builder.py @@ -237,10 +237,7 @@ def build_crs_filter( price_base = self._to_filter_str(price_base) unit_measure = self._to_filter_str(unit_measure) md_id = self._to_filter_str(None) - if microdata: - md_dim = "DD" - else: - md_dim = "_T" + md_dim = "DD" if microdata else "_T" return ".".join( [ donor, @@ -266,7 +263,7 @@ def build_multisystem_filter( channel: int | list[int] | None = None, flow_type: str | list[str] | None = None, price_base: str | list[str] | None = None, - ): + ) -> str: """Build the filter string for the Multisystem dataflow. The allowed filter follows the pattern: diff --git a/src/oda_reader/multisystem.py b/src/oda_reader/multisystem.py index a8dc15e..ba3731d 100644 --- a/src/oda_reader/multisystem.py +++ b/src/oda_reader/multisystem.py @@ -6,10 +6,10 @@ from oda_reader._cache import cache_info from oda_reader.common import logger from oda_reader.download.download_tools import ( - get_bulk_file_id, MULTI_FLOW_URL, bulk_download_parquet, download, + get_bulk_file_id, ) DATAFLOW_ID: str = "DSD_MULTI@DF_MULTI" @@ -33,10 +33,8 @@ def bulk_download_multisystem( provided, the function will return a DataFrame. Args: - save_to_path: The path to save the file to. Optional. If not provided, a - DataFrame is returned. - as_iterator: If `True` yields `DataFrame` chunks instead of a single - `DataFrame`. + save_to_path: The path to save the file to. Optional. If not provided, a DataFrame is returned. + as_iterator: If `True` yields `DataFrame` chunks instead of a single `DataFrame`. Returns: @@ -70,8 +68,7 @@ def download_multisystem( start_year (int): The start year of the data to download. Optional end_year (int): The end year of the data to download. Optional filters (dict): Optional filters to pass to the download. - pre_process (bool): Whether to preprocess the data. Defaults to True. - Preprocessing makes it comply with the .stat schema. + pre_process (bool): Whether to preprocess the data. Defaults to True. Preprocessing makes it comply with the .stat schema. dotstat_codes (bool): Whether to convert the donor codes to the .stat schema. dataflow_version (str): The version of the dataflow. Optional diff --git a/src/oda_reader/schemas/crs_translation.py b/src/oda_reader/schemas/crs_translation.py index a126e26..a50f6ed 100644 --- a/src/oda_reader/schemas/crs_translation.py +++ b/src/oda_reader/schemas/crs_translation.py @@ -2,7 +2,7 @@ from oda_reader.common import ImporterPaths from oda_reader.schemas.dac1_translation import prices_mapping -from oda_reader.schemas.schema_tools import map_area_codes, map_amount_type_codes +from oda_reader.schemas.schema_tools import map_amount_type_codes, map_area_codes from oda_reader.schemas.xml_tools import ( read_mapping, ) diff --git a/src/oda_reader/schemas/dac1_translation.py b/src/oda_reader/schemas/dac1_translation.py index b944dc3..02d45ad 100644 --- a/src/oda_reader/schemas/dac1_translation.py +++ b/src/oda_reader/schemas/dac1_translation.py @@ -2,15 +2,15 @@ from oda_reader.common import ImporterPaths from oda_reader.schemas.schema_tools import ( - map_area_codes, convert_unit_measure_to_amount_type, map_amount_type_codes, + map_area_codes, ) from oda_reader.schemas.xml_tools import ( - parse_xml, extract_dac_to_area_codes, extract_datatypes_to_prices_codes, extract_flowtype_to_flowtype_codes, + parse_xml, read_mapping, ) diff --git a/src/oda_reader/schemas/dac2_translation.py b/src/oda_reader/schemas/dac2_translation.py index a5af722..0c6678b 100644 --- a/src/oda_reader/schemas/dac2_translation.py +++ b/src/oda_reader/schemas/dac2_translation.py @@ -2,10 +2,10 @@ from oda_reader.common import ImporterPaths from oda_reader.schemas.dac1_translation import prices_mapping -from oda_reader.schemas.schema_tools import map_area_codes, map_amount_type_codes +from oda_reader.schemas.schema_tools import map_amount_type_codes, map_area_codes from oda_reader.schemas.xml_tools import ( - parse_xml, extract_dac_to_area_codes, + parse_xml, read_mapping, ) diff --git a/src/oda_reader/schemas/mappings/aidData_schema.json b/src/oda_reader/schemas/mappings/aidData_schema.json index 56c47fb..7220641 100644 --- a/src/oda_reader/schemas/mappings/aidData_schema.json +++ b/src/oda_reader/schemas/mappings/aidData_schema.json @@ -491,7 +491,7 @@ "keep": true, "bool": true }, - "Supplier’s Credit/Export Seller’s Credit": { + "Supplier\u2019s Credit/Export Seller\u2019s Credit": { "name": "suppliers_credit_export_sellers_credit", "type": "string[pyarrow]", "keep": true, @@ -539,7 +539,7 @@ "keep": true, "bool": true }, - "FXSL/BOP": { + "FXSL/BOP": { "name": "fxsl_bop", "type": "string[pyarrow]", "keep": true, @@ -755,4 +755,4 @@ "keep": true, "bool": false } -} \ No newline at end of file +} diff --git a/src/oda_reader/schemas/mappings/area_code_corrections.json b/src/oda_reader/schemas/mappings/area_code_corrections.json index 93e6aeb..4443c57 100644 --- a/src/oda_reader/schemas/mappings/area_code_corrections.json +++ b/src/oda_reader/schemas/mappings/area_code_corrections.json @@ -7,4 +7,4 @@ "10350": "O9_X", "918": "4EU001", "20004": "DACEU_EC" -} \ No newline at end of file +} diff --git a/src/oda_reader/schemas/mappings/code_prices_corrections.json b/src/oda_reader/schemas/mappings/code_prices_corrections.json index 540e44b..b745c16 100644 --- a/src/oda_reader/schemas/mappings/code_prices_corrections.json +++ b/src/oda_reader/schemas/mappings/code_prices_corrections.json @@ -1,4 +1,4 @@ { "XDC": "N", "PT_B5G": "PT_B5G" -} \ No newline at end of file +} diff --git a/src/oda_reader/schemas/mappings/crs_dotstat.json b/src/oda_reader/schemas/mappings/crs_dotstat.json index 7c8ff9d..2a61fb9 100644 --- a/src/oda_reader/schemas/mappings/crs_dotstat.json +++ b/src/oda_reader/schemas/mappings/crs_dotstat.json @@ -505,5 +505,3 @@ "keep": false } } - - diff --git a/src/oda_reader/schemas/mappings/dac1_codes_area.json b/src/oda_reader/schemas/mappings/dac1_codes_area.json index dfb6a67..6497af1 100644 --- a/src/oda_reader/schemas/mappings/dac1_codes_area.json +++ b/src/oda_reader/schemas/mappings/dac1_codes_area.json @@ -1,418 +1,418 @@ { - "801": "AUS", - "1": "AUT", - "2": "BEL", - "301": "CAN", - "434": "CHL", - "437": "COL", - "336": "CRI", - "68": "CZE", - "3": "DNK", - "82": "EST", - "18": "FIN", - "4": "FRA", - "5": "DEU", - "40": "GRC", - "75": "HUN", - "20": "ISL", - "21": "IRL", - "546": "ISR", - "6": "ITA", - "701": "JPN", - "742": "KOR", - "83": "LVA", - "84": "LTU", - "22": "LUX", - "358": "MEX", - "7": "NLD", - "820": "NZL", - "8": "NOR", - "76": "POL", - "9": "PRT", - "69": "SVK", - "61": "SVN", - "50": "ESP", - "10": "SWE", - "11": "CHE", - "55": "TUR", - "12": "GBR", - "302": "USA", - "625": "AFG", - "71": "ALB", - "130": "DZA", - "225": "AGO", - "376": "AIA", - "377": "ATG", - "425": "ARG", - "610": "ARM", - "373": "ABW", - "611": "AZE", - "328": "BHS", - "530": "BHR", - "666": "BGD", - "329": "BRB", - "86": "BLR", - "352": "BLZ", - "236": "BEN", - "331": "BMU", - "630": "BTN", - "428": "BOL", - "64": "BIH", - "227": "BWA", - "431": "BRA", - "388": "VGB", - "725": "BRN", - "72": "BGR", - "287": "BFA", - "228": "BDI", - "230": "CPV", - "728": "KHM", - "229": "CMR", - "386": "CYM", - "231": "CAF", - "232": "TCD", - "730": "CHN", - "233": "COM", - "234": "COG", - "831": "COK", - "247": "CIV", - "62": "HRV", - "338": "CUB", - "30": "CYP", - "740": "PRK", - "235": "COD", - "274": "DJI", - "378": "DMA", - "340": "DOM", - "440": "ECU", - "142": "EGY", - "342": "SLV", - "245": "GNQ", - "271": "ERI", - "280": "SWZ", - "238": "ETH", - "443": "FLK", - "832": "FJI", - "840": "PYF", - "239": "GAB", - "240": "GMB", - "612": "GEO", - "241": "GHA", - "35": "GIB", - "381": "GRD", - "347": "GTM", - "243": "GIN", - "244": "GNB", - "446": "GUY", - "349": "HTI", - "351": "HND", - "735": "HKG", - "645": "IND", - "738": "IDN", - "540": "IRN", - "543": "IRQ", - "354": "JAM", - "549": "JOR", - "613": "KAZ", - "248": "KEN", - "836": "KIR", - "57": "XKV", - "552": "KWT", - "614": "KGZ", - "745": "LAO", - "555": "LBN", - "249": "LSO", - "251": "LBR", - "133": "LBY", - "70": "LIE", - "748": "MAC", - "252": "MDG", - "253": "MWI", - "751": "MYS", - "655": "MDV", - "255": "MLI", - "45": "MLT", - "859": "MHL", - "256": "MRT", - "257": "MUS", - "258": "MYT", - "860": "FSM", - "93": "MDA", - "26": "MCO", - "753": "MNG", - "65": "MNE", - "385": "MSR", - "136": "MAR", - "259": "MOZ", - "635": "MMR", - "275": "NAM", - "845": "NRU", - "660": "NPL", - "850": "NCL", - "364": "NIC", - "260": "NER", - "261": "NGA", - "856": "NIU", - "66": "MKD", - "858": "MNP", - "558": "OMN", - "665": "PAK", - "861": "PLW", - "550": "PSE", - "366": "PAN", - "862": "PNG", - "451": "PRY", - "454": "PER", - "755": "PHL", - "561": "QAT", - "77": "ROU", - "87": "RUS", - "266": "RWA", - "276": "SHN", - "382": "KNA", - "383": "LCA", - "384": "VCT", - "880": "WSM", - "268": "STP", - "566": "SAU", - "269": "SEN", - "63": "SRB", - "270": "SYC", - "272": "SLE", - "761": "SGP", - "866": "SLB", - "273": "SOM", - "218": "ZAF", - "279": "SSD", - "640": "LKA", - "278": "SDN", - "457": "SUR", - "573": "SYR", - "732": "TWN", - "615": "TJK", - "282": "TZA", - "764": "THA", - "765": "TLS", - "283": "TGO", - "868": "TKL", - "870": "TON", - "375": "TTO", - "139": "TUN", - "616": "TKM", - "387": "TCA", - "872": "TUV", - "285": "UGA", - "85": "UKR", - "576": "ARE", - "460": "URY", - "617": "UZB", - "854": "VUT", - "463": "VEN", - "769": "VNM", - "876": "WLF", - "580": "YEM", - "288": "ZMB", - "265": "ZWE", - "10040": "ACP", - "10202": "SIDS", - "10203": "FSCAC", - "10004": "A", - "10056": "A2", - "10055": "A3", - "10054": "A4", - "10006": "A7", - "10010": "E", - "10001": "F", - "10050": "F3", - "10051": "F5", - "10052": "F7", - "10053": "F8", - "10011": "F97", - "10012": "O", - "10057": "O7", - "10058": "O8", - "10059": "O9", - "10007": "S", - "20005": "ALLD", - "20002": "ALLM", - "10200": "ALLR", - "10150": "ALLMR", - "20918": "4EU001", - "917": "4EU002", - "918": "4EU003", - "919": "4EU004", - "927": "4EU999", - "20013": "5IMF0", - "907": "5IMF01", - "958": "5IMF02", - "20022": "5RDB0", - "20016": "5AFDB0", - "913": "5AFDB001", - "914": "5AFDB002", - "20015": "5ASDB0", - "915": "5ASDB01", - "916": "5ASDB02", - "20014": "5IDB0", - "909": "5IDB001", - "912": "5IDB002", - "1019": "5IDB003", - "1024": "5RDB001", - "981": "5RDB002", - "910": "5RDB003", - "906": "5RDB004", - "1013": "5RDB005", - "1015": "5RDB006", - "990": "5RDB007", - "1037": "5RDB008", - "976": "5RDB009", - "1044": "5RDB010", - "1045": "5RDB011", - "816": "5RDB999", - "20021": "1UN0", - "1020": "1UN001", - "1047": "1UN002", - "932": "1UN003", - "988": "1UN004", - "944": "1UN005", - "940": "1UN006", - "1048": "1UN007", - "1039": "1UN008", - "965": "1UN009", - "1038": "1UN010", - "923": "1UN011", - "1046": "1UN012", - "971": "1UN013", - "959": "1UN014", - "948": "1UN015", - "807": "1UN016", - "974": "1UN017", - "967": "1UN018", - "963": "1UN019", - "962": "1UN020", - "1406": "1UN021", - "964": "1UN022", - "960": "1UN023", - "966": "1UN024", - "1050": "1UN025", - "928": "1UN026", - "1023": "1UN027", - "1404": "1UN028", - "1401": "1UN029", - "975": "1UN999", - "20012": "5WBG0", - "20033": "5WB0", - "901": "5WB001", - "905": "5WB002", - "904": "5WB003", - "902": "5WB004", - "903": "5WBG002", - "900": "5WBG003", - "20034": "9OTH0", - "1012": "9OTH001", - "953": "9OTH002", - "921": "9OTH003", - "1053": "9OTH004", - "1025": "9OTH005", - "1011": "9OTH006", - "1018": "9OTH010", - "1041": "9OTH008", - "1311": "9OTH009", - "811": "9OTH011", - "1312": "9OTH012", - "1313": "9OTH013", - "997": "9OTH014", - "1016": "9OTH015", - "1052": "9OTH016", - "1049": "9OTH017", - "812": "9OTH018", - "104": "9OTH019", - "951": "9OTH020", - "978": "9OTH021", - "1014": "9OTH022", - "1054": "9OTH023", - "989": "9OTH998", - "20018": "9OTH999", - "21600": "9PRIV0", - "1637": "9PRIV1637", - "1625": "9PRIV1625", - "1609": "9PRIV1609", - "1646": "9PRIV1646", - "1601": "9PRIV1601", - "1640": "9PRIV1640", - "1620": "9PRIV1620", - "1645": "9PRIV1645", - "1614": "9PRIV1614", - "1615": "9PRIV1615", - "1632": "9PRIV1632", - "1617": "9PRIV1617", - "1618": "9PRIV1618", - "1644": "9PRIV1644", - "1627": "9PRIV1627", - "1616": "9PRIV1616", - "1626": "9PRIV1626", - "1612": "9PRIV1612", - "1642": "9PRIV1642", - "1608": "9PRIV1608", - "1636": "9PRIV1636", - "1619": "9PRIV1619", - "1639": "9PRIV1639", - "1613": "9PRIV1613", - "1633": "9PRIV1633", - "1648": "9PRIV1648", - "1638": "9PRIV1638", - "1606": "9PRIV1606", - "1610": "9PRIV1610", - "1631": "9PRIV1631", - "1605": "9PRIV1605", - "1621": "9PRIV1621", - "1611": "9PRIV1611", - "1622": "9PRIV1622", - "1643": "9PRIV1643", - "1623": "9PRIV1623", - "1641": "9PRIV1641", - "1629": "9PRIV1629", - "1628": "9PRIV1628", - "1624": "9PRIV1624", - "1630": "9PRIV1630", - "20035": "9PLG0", - "1602": "9PLG0_NL", - "1647": "9PLG0_DE", - "1634": "9PLG0_NO", - "1604": "9PLG0_GB", - "1603": "9PLG0_SE", - "20001": "DAC", - "20003": "G7", - "20004": "DACEUEC", - "20006": "WXDAC", - "20007": "W_O", - "20011": "DACEU", - "60": "YUG_F", - "88": "SFRY_X", - "89": "E_X", - "298": "F_X", - "361": "SXM", - "489": "A7_X", - "498": "A_X", - "789": "S97_X", - "798": "S_X", - "889": "O_X", - "9998": "DPGC_X", - "10002": "F4", - "10003": "F6", - "10005": "A5", - "10016": "LDC", - "10017": "OLIC", - "10018": "LMIC", - "10019": "UMIC", - "10024": "INC_X", - "10025": "MADCT", - "10030": "HIPC", - "10045": "OLICWB", - "10046": "LMICWB", - "10047": "UMICWB", - "10048": "HICSWB", - "10049": "INCWB_X", - "10100": "DPGC", - "10201": "LLDC", - "(.*)": "\\1" -} \ No newline at end of file + "801": "AUS", + "1": "AUT", + "2": "BEL", + "301": "CAN", + "434": "CHL", + "437": "COL", + "336": "CRI", + "68": "CZE", + "3": "DNK", + "82": "EST", + "18": "FIN", + "4": "FRA", + "5": "DEU", + "40": "GRC", + "75": "HUN", + "20": "ISL", + "21": "IRL", + "546": "ISR", + "6": "ITA", + "701": "JPN", + "742": "KOR", + "83": "LVA", + "84": "LTU", + "22": "LUX", + "358": "MEX", + "7": "NLD", + "820": "NZL", + "8": "NOR", + "76": "POL", + "9": "PRT", + "69": "SVK", + "61": "SVN", + "50": "ESP", + "10": "SWE", + "11": "CHE", + "55": "TUR", + "12": "GBR", + "302": "USA", + "625": "AFG", + "71": "ALB", + "130": "DZA", + "225": "AGO", + "376": "AIA", + "377": "ATG", + "425": "ARG", + "610": "ARM", + "373": "ABW", + "611": "AZE", + "328": "BHS", + "530": "BHR", + "666": "BGD", + "329": "BRB", + "86": "BLR", + "352": "BLZ", + "236": "BEN", + "331": "BMU", + "630": "BTN", + "428": "BOL", + "64": "BIH", + "227": "BWA", + "431": "BRA", + "388": "VGB", + "725": "BRN", + "72": "BGR", + "287": "BFA", + "228": "BDI", + "230": "CPV", + "728": "KHM", + "229": "CMR", + "386": "CYM", + "231": "CAF", + "232": "TCD", + "730": "CHN", + "233": "COM", + "234": "COG", + "831": "COK", + "247": "CIV", + "62": "HRV", + "338": "CUB", + "30": "CYP", + "740": "PRK", + "235": "COD", + "274": "DJI", + "378": "DMA", + "340": "DOM", + "440": "ECU", + "142": "EGY", + "342": "SLV", + "245": "GNQ", + "271": "ERI", + "280": "SWZ", + "238": "ETH", + "443": "FLK", + "832": "FJI", + "840": "PYF", + "239": "GAB", + "240": "GMB", + "612": "GEO", + "241": "GHA", + "35": "GIB", + "381": "GRD", + "347": "GTM", + "243": "GIN", + "244": "GNB", + "446": "GUY", + "349": "HTI", + "351": "HND", + "735": "HKG", + "645": "IND", + "738": "IDN", + "540": "IRN", + "543": "IRQ", + "354": "JAM", + "549": "JOR", + "613": "KAZ", + "248": "KEN", + "836": "KIR", + "57": "XKV", + "552": "KWT", + "614": "KGZ", + "745": "LAO", + "555": "LBN", + "249": "LSO", + "251": "LBR", + "133": "LBY", + "70": "LIE", + "748": "MAC", + "252": "MDG", + "253": "MWI", + "751": "MYS", + "655": "MDV", + "255": "MLI", + "45": "MLT", + "859": "MHL", + "256": "MRT", + "257": "MUS", + "258": "MYT", + "860": "FSM", + "93": "MDA", + "26": "MCO", + "753": "MNG", + "65": "MNE", + "385": "MSR", + "136": "MAR", + "259": "MOZ", + "635": "MMR", + "275": "NAM", + "845": "NRU", + "660": "NPL", + "850": "NCL", + "364": "NIC", + "260": "NER", + "261": "NGA", + "856": "NIU", + "66": "MKD", + "858": "MNP", + "558": "OMN", + "665": "PAK", + "861": "PLW", + "550": "PSE", + "366": "PAN", + "862": "PNG", + "451": "PRY", + "454": "PER", + "755": "PHL", + "561": "QAT", + "77": "ROU", + "87": "RUS", + "266": "RWA", + "276": "SHN", + "382": "KNA", + "383": "LCA", + "384": "VCT", + "880": "WSM", + "268": "STP", + "566": "SAU", + "269": "SEN", + "63": "SRB", + "270": "SYC", + "272": "SLE", + "761": "SGP", + "866": "SLB", + "273": "SOM", + "218": "ZAF", + "279": "SSD", + "640": "LKA", + "278": "SDN", + "457": "SUR", + "573": "SYR", + "732": "TWN", + "615": "TJK", + "282": "TZA", + "764": "THA", + "765": "TLS", + "283": "TGO", + "868": "TKL", + "870": "TON", + "375": "TTO", + "139": "TUN", + "616": "TKM", + "387": "TCA", + "872": "TUV", + "285": "UGA", + "85": "UKR", + "576": "ARE", + "460": "URY", + "617": "UZB", + "854": "VUT", + "463": "VEN", + "769": "VNM", + "876": "WLF", + "580": "YEM", + "288": "ZMB", + "265": "ZWE", + "10040": "ACP", + "10202": "SIDS", + "10203": "FSCAC", + "10004": "A", + "10056": "A2", + "10055": "A3", + "10054": "A4", + "10006": "A7", + "10010": "E", + "10001": "F", + "10050": "F3", + "10051": "F5", + "10052": "F7", + "10053": "F8", + "10011": "F97", + "10012": "O", + "10057": "O7", + "10058": "O8", + "10059": "O9", + "10007": "S", + "20005": "ALLD", + "20002": "ALLM", + "10200": "ALLR", + "10150": "ALLMR", + "20918": "4EU001", + "917": "4EU002", + "918": "4EU003", + "919": "4EU004", + "927": "4EU999", + "20013": "5IMF0", + "907": "5IMF01", + "958": "5IMF02", + "20022": "5RDB0", + "20016": "5AFDB0", + "913": "5AFDB001", + "914": "5AFDB002", + "20015": "5ASDB0", + "915": "5ASDB01", + "916": "5ASDB02", + "20014": "5IDB0", + "909": "5IDB001", + "912": "5IDB002", + "1019": "5IDB003", + "1024": "5RDB001", + "981": "5RDB002", + "910": "5RDB003", + "906": "5RDB004", + "1013": "5RDB005", + "1015": "5RDB006", + "990": "5RDB007", + "1037": "5RDB008", + "976": "5RDB009", + "1044": "5RDB010", + "1045": "5RDB011", + "816": "5RDB999", + "20021": "1UN0", + "1020": "1UN001", + "1047": "1UN002", + "932": "1UN003", + "988": "1UN004", + "944": "1UN005", + "940": "1UN006", + "1048": "1UN007", + "1039": "1UN008", + "965": "1UN009", + "1038": "1UN010", + "923": "1UN011", + "1046": "1UN012", + "971": "1UN013", + "959": "1UN014", + "948": "1UN015", + "807": "1UN016", + "974": "1UN017", + "967": "1UN018", + "963": "1UN019", + "962": "1UN020", + "1406": "1UN021", + "964": "1UN022", + "960": "1UN023", + "966": "1UN024", + "1050": "1UN025", + "928": "1UN026", + "1023": "1UN027", + "1404": "1UN028", + "1401": "1UN029", + "975": "1UN999", + "20012": "5WBG0", + "20033": "5WB0", + "901": "5WB001", + "905": "5WB002", + "904": "5WB003", + "902": "5WB004", + "903": "5WBG002", + "900": "5WBG003", + "20034": "9OTH0", + "1012": "9OTH001", + "953": "9OTH002", + "921": "9OTH003", + "1053": "9OTH004", + "1025": "9OTH005", + "1011": "9OTH006", + "1018": "9OTH010", + "1041": "9OTH008", + "1311": "9OTH009", + "811": "9OTH011", + "1312": "9OTH012", + "1313": "9OTH013", + "997": "9OTH014", + "1016": "9OTH015", + "1052": "9OTH016", + "1049": "9OTH017", + "812": "9OTH018", + "104": "9OTH019", + "951": "9OTH020", + "978": "9OTH021", + "1014": "9OTH022", + "1054": "9OTH023", + "989": "9OTH998", + "20018": "9OTH999", + "21600": "9PRIV0", + "1637": "9PRIV1637", + "1625": "9PRIV1625", + "1609": "9PRIV1609", + "1646": "9PRIV1646", + "1601": "9PRIV1601", + "1640": "9PRIV1640", + "1620": "9PRIV1620", + "1645": "9PRIV1645", + "1614": "9PRIV1614", + "1615": "9PRIV1615", + "1632": "9PRIV1632", + "1617": "9PRIV1617", + "1618": "9PRIV1618", + "1644": "9PRIV1644", + "1627": "9PRIV1627", + "1616": "9PRIV1616", + "1626": "9PRIV1626", + "1612": "9PRIV1612", + "1642": "9PRIV1642", + "1608": "9PRIV1608", + "1636": "9PRIV1636", + "1619": "9PRIV1619", + "1639": "9PRIV1639", + "1613": "9PRIV1613", + "1633": "9PRIV1633", + "1648": "9PRIV1648", + "1638": "9PRIV1638", + "1606": "9PRIV1606", + "1610": "9PRIV1610", + "1631": "9PRIV1631", + "1605": "9PRIV1605", + "1621": "9PRIV1621", + "1611": "9PRIV1611", + "1622": "9PRIV1622", + "1643": "9PRIV1643", + "1623": "9PRIV1623", + "1641": "9PRIV1641", + "1629": "9PRIV1629", + "1628": "9PRIV1628", + "1624": "9PRIV1624", + "1630": "9PRIV1630", + "20035": "9PLG0", + "1602": "9PLG0_NL", + "1647": "9PLG0_DE", + "1634": "9PLG0_NO", + "1604": "9PLG0_GB", + "1603": "9PLG0_SE", + "20001": "DAC", + "20003": "G7", + "20004": "DACEUEC", + "20006": "WXDAC", + "20007": "W_O", + "20011": "DACEU", + "60": "YUG_F", + "88": "SFRY_X", + "89": "E_X", + "298": "F_X", + "361": "SXM", + "489": "A7_X", + "498": "A_X", + "789": "S97_X", + "798": "S_X", + "889": "O_X", + "9998": "DPGC_X", + "10002": "F4", + "10003": "F6", + "10005": "A5", + "10016": "LDC", + "10017": "OLIC", + "10018": "LMIC", + "10019": "UMIC", + "10024": "INC_X", + "10025": "MADCT", + "10030": "HIPC", + "10045": "OLICWB", + "10046": "LMICWB", + "10047": "UMICWB", + "10048": "HICSWB", + "10049": "INCWB_X", + "10100": "DPGC", + "10201": "LLDC", + "(.*)": "\\1" +} diff --git a/src/oda_reader/schemas/mappings/dac1_codes_flow_types.json b/src/oda_reader/schemas/mappings/dac1_codes_flow_types.json index c2cc204..a79f1a7 100644 --- a/src/oda_reader/schemas/mappings/dac1_codes_flow_types.json +++ b/src/oda_reader/schemas/mappings/dac1_codes_flow_types.json @@ -1,5 +1,5 @@ { - "115": "C", - "112": "D", - "(.*)": "\\1" -} \ No newline at end of file + "115": "C", + "112": "D", + "(.*)": "\\1" +} diff --git a/src/oda_reader/schemas/mappings/dac1_codes_prices.json b/src/oda_reader/schemas/mappings/dac1_codes_prices.json index aea3a31..3a2aa74 100644 --- a/src/oda_reader/schemas/mappings/dac1_codes_prices.json +++ b/src/oda_reader/schemas/mappings/dac1_codes_prices.json @@ -1,4 +1,4 @@ { - "V": "A", - "Q": "D" -} \ No newline at end of file + "V": "A", + "Q": "D" +} diff --git a/src/oda_reader/schemas/mappings/dac2_codes_area.json b/src/oda_reader/schemas/mappings/dac2_codes_area.json index 600c8f2..8d399e1 100644 --- a/src/oda_reader/schemas/mappings/dac2_codes_area.json +++ b/src/oda_reader/schemas/mappings/dac2_codes_area.json @@ -1,432 +1,432 @@ { - "801": "AUS", - "1": "AUT", - "2": "BEL", - "301": "CAN", - "434": "CHL", - "437": "COL", - "336": "CRI", - "68": "CZE", - "3": "DNK", - "82": "EST", - "18": "FIN", - "4": "FRA", - "5": "DEU", - "40": "GRC", - "75": "HUN", - "20": "ISL", - "21": "IRL", - "546": "ISR", - "6": "ITA", - "701": "JPN", - "742": "KOR", - "83": "LVA", - "84": "LTU", - "22": "LUX", - "358": "MEX", - "7": "NLD", - "820": "NZL", - "8": "NOR", - "76": "POL", - "9": "PRT", - "69": "SVK", - "61": "SVN", - "50": "ESP", - "10": "SWE", - "11": "CHE", - "55": "TUR", - "12": "GBR", - "302": "USA", - "625": "AFG", - "71": "ALB", - "130": "DZA", - "225": "AGO", - "376": "AIA", - "377": "ATG", - "425": "ARG", - "610": "ARM", - "373": "ABW", - "611": "AZE", - "328": "BHS", - "530": "BHR", - "666": "BGD", - "329": "BRB", - "86": "BLR", - "352": "BLZ", - "236": "BEN", - "331": "BMU", - "630": "BTN", - "428": "BOL", - "64": "BIH", - "227": "BWA", - "431": "BRA", - "388": "VGB", - "725": "BRN", - "72": "BGR", - "287": "BFA", - "228": "BDI", - "230": "CPV", - "728": "KHM", - "229": "CMR", - "386": "CYM", - "231": "CAF", - "232": "TCD", - "730": "CHN", - "233": "COM", - "234": "COG", - "831": "COK", - "247": "CIV", - "62": "HRV", - "338": "CUB", - "30": "CYP", - "740": "PRK", - "235": "COD", - "274": "DJI", - "378": "DMA", - "340": "DOM", - "440": "ECU", - "142": "EGY", - "342": "SLV", - "245": "GNQ", - "271": "ERI", - "280": "SWZ", - "238": "ETH", - "443": "FLK", - "832": "FJI", - "840": "PYF", - "239": "GAB", - "240": "GMB", - "612": "GEO", - "241": "GHA", - "35": "GIB", - "381": "GRD", - "347": "GTM", - "243": "GIN", - "244": "GNB", - "446": "GUY", - "349": "HTI", - "351": "HND", - "735": "HKG", - "645": "IND", - "738": "IDN", - "540": "IRN", - "543": "IRQ", - "354": "JAM", - "549": "JOR", - "613": "KAZ", - "248": "KEN", - "836": "KIR", - "57": "XKV", - "552": "KWT", - "614": "KGZ", - "745": "LAO", - "555": "LBN", - "249": "LSO", - "251": "LBR", - "133": "LBY", - "70": "LIE", - "748": "MAC", - "252": "MDG", - "253": "MWI", - "751": "MYS", - "655": "MDV", - "255": "MLI", - "45": "MLT", - "859": "MHL", - "256": "MRT", - "257": "MUS", - "258": "MYT", - "860": "FSM", - "93": "MDA", - "26": "MCO", - "753": "MNG", - "65": "MNE", - "385": "MSR", - "136": "MAR", - "259": "MOZ", - "635": "MMR", - "275": "NAM", - "845": "NRU", - "660": "NPL", - "850": "NCL", - "364": "NIC", - "260": "NER", - "261": "NGA", - "856": "NIU", - "66": "MKD", - "858": "MNP", - "558": "OMN", - "665": "PAK", - "861": "PLW", - "550": "PSE", - "366": "PAN", - "862": "PNG", - "451": "PRY", - "454": "PER", - "755": "PHL", - "561": "QAT", - "77": "ROU", - "87": "RUS", - "266": "RWA", - "276": "SHN", - "382": "KNA", - "383": "LCA", - "384": "VCT", - "880": "WSM", - "268": "STP", - "566": "SAU", - "269": "SEN", - "63": "SRB", - "270": "SYC", - "272": "SLE", - "761": "SGP", - "866": "SLB", - "273": "SOM", - "218": "ZAF", - "279": "SSD", - "640": "LKA", - "278": "SDN", - "457": "SUR", - "573": "SYR", - "732": "TWN", - "615": "TJK", - "282": "TZA", - "764": "THA", - "765": "TLS", - "283": "TGO", - "868": "TKL", - "870": "TON", - "375": "TTO", - "139": "TUN", - "616": "TKM", - "387": "TCA", - "872": "TUV", - "285": "UGA", - "85": "UKR", - "576": "ARE", - "460": "URY", - "617": "UZB", - "854": "VUT", - "463": "VEN", - "769": "VNM", - "876": "WLF", - "580": "YEM", - "288": "ZMB", - "265": "ZWE", - "10040": "ACP", - "10202": "SIDS", - "10203": "FSCAC", - "10004": "A", - "10056": "A2", - "10055": "A3", - "10054": "A4", - "10006": "A7", - "10010": "E", - "10001": "F", - "10050": "F3", - "10051": "F5", - "10052": "F7", - "10053": "F8", - "10011": "F97", - "10012": "O", - "10057": "O7", - "10058": "O8", - "10059": "O9", - "10007": "S", - "20005": "ALLD", - "20002": "ALLM", - "10200": "ALLR", - "10150": "ALLMR", - "20918": "4EU001", - "917": "4EU002", - "918": "4EU003", - "919": "4EU004", - "927": "4EU999", - "20013": "5IMF0", - "907": "5IMF01", - "958": "5IMF02", - "20022": "5RDB0", - "20016": "5AFDB0", - "913": "5AFDB001", - "914": "5AFDB002", - "20015": "5ASDB0", - "915": "5ASDB01", - "916": "5ASDB02", - "20014": "5IDB0", - "909": "5IDB001", - "912": "5IDB002", - "1019": "5IDB003", - "1024": "5RDB001", - "981": "5RDB002", - "910": "5RDB003", - "906": "5RDB004", - "1013": "5RDB005", - "1015": "5RDB006", - "990": "5RDB007", - "1037": "5RDB008", - "976": "5RDB009", - "1044": "5RDB010", - "1045": "5RDB011", - "816": "5RDB999", - "20021": "1UN0", - "1020": "1UN001", - "1047": "1UN002", - "932": "1UN003", - "988": "1UN004", - "944": "1UN005", - "940": "1UN006", - "1048": "1UN007", - "1039": "1UN008", - "965": "1UN009", - "1038": "1UN010", - "923": "1UN011", - "1046": "1UN012", - "971": "1UN013", - "959": "1UN014", - "948": "1UN015", - "807": "1UN016", - "974": "1UN017", - "967": "1UN018", - "963": "1UN019", - "962": "1UN020", - "1406": "1UN021", - "964": "1UN022", - "960": "1UN023", - "966": "1UN024", - "1050": "1UN025", - "928": "1UN026", - "1023": "1UN027", - "1404": "1UN028", - "1401": "1UN029", - "975": "1UN999", - "20012": "5WBG0", - "20033": "5WB0", - "901": "5WB001", - "905": "5WB002", - "904": "5WB003", - "902": "5WB004", - "903": "5WBG002", - "900": "5WBG003", - "20034": "9OTH0", - "1012": "9OTH001", - "953": "9OTH002", - "921": "9OTH003", - "1053": "9OTH004", - "1025": "9OTH005", - "1011": "9OTH006", - "1018": "9OTH010", - "1041": "9OTH008", - "1311": "9OTH009", - "811": "9OTH011", - "1312": "9OTH012", - "1313": "9OTH013", - "997": "9OTH014", - "1016": "9OTH015", - "1052": "9OTH016", - "1049": "9OTH017", - "812": "9OTH018", - "104": "9OTH019", - "951": "9OTH020", - "978": "9OTH021", - "1014": "9OTH022", - "1054": "9OTH023", - "989": "9OTH998", - "20018": "9OTH999", - "21600": "9PRIV0", - "1637": "9PRIV1637", - "1625": "9PRIV1625", - "1609": "9PRIV1609", - "1646": "9PRIV1646", - "1601": "9PRIV1601", - "1640": "9PRIV1640", - "1620": "9PRIV1620", - "1645": "9PRIV1645", - "1614": "9PRIV1614", - "1615": "9PRIV1615", - "1632": "9PRIV1632", - "1617": "9PRIV1617", - "1618": "9PRIV1618", - "1644": "9PRIV1644", - "1627": "9PRIV1627", - "1616": "9PRIV1616", - "1626": "9PRIV1626", - "1612": "9PRIV1612", - "1642": "9PRIV1642", - "1608": "9PRIV1608", - "1636": "9PRIV1636", - "1619": "9PRIV1619", - "1639": "9PRIV1639", - "1613": "9PRIV1613", - "1633": "9PRIV1633", - "1648": "9PRIV1648", - "1638": "9PRIV1638", - "1606": "9PRIV1606", - "1610": "9PRIV1610", - "1631": "9PRIV1631", - "1605": "9PRIV1605", - "1621": "9PRIV1621", - "1611": "9PRIV1611", - "1622": "9PRIV1622", - "1643": "9PRIV1643", - "1623": "9PRIV1623", - "1641": "9PRIV1641", - "1629": "9PRIV1629", - "1628": "9PRIV1628", - "1624": "9PRIV1624", - "1630": "9PRIV1630", - "20035": "9PLG0", - "1602": "9PLG0_NL", - "1647": "9PLG0_DE", - "1634": "9PLG0_NO", - "1604": "9PLG0_GB", - "1603": "9PLG0_SE", - "20001": "DAC", - "20003": "G7", - "20004": "DACEU_EC", - "20006": "WXDAC", - "20007": "W_O", - "20011": "DACEU", - "60": "YUG_F", - "88": "SFRY_X", - "89": "E_X", - "298": "F_X", - "361": "SXM", - "489": "A7_X", - "498": "A_X", - "789": "S97_X", - "798": "S_X", - "889": "O_X", - "9998": "DPGC_X", - "10002": "F4", - "10003": "F6", - "10005": "A5", - "10016": "LDC", - "10017": "OLIC", - "10018": "LMIC", - "10019": "UMIC", - "10024": "INC_X", - "10025": "MADCT", - "10030": "HIPC", - "10045": "OLICWB", - "10046": "LMICWB", - "10047": "UMICWB", - "10048": "HICSWB", - "10049": "INCWB_X", - "10100": "DPGC", - "10201": "LLDC", - "189": "F4_X", - "289": "F6_X", - "389": "A5_X", - "589": "F97_X", - "619": "S4_X", - "689": "S4_S7_X", - "1027": "F3_X", - "1029": "F7_X", - "1030": "F8_X", - "1031": "A4_X", - "1032": "A3_X", - "10008": "S97", - "10009": "S4_S7", - "1055": "9OTH024", - "(.*)": "\\1" -} \ No newline at end of file + "801": "AUS", + "1": "AUT", + "2": "BEL", + "301": "CAN", + "434": "CHL", + "437": "COL", + "336": "CRI", + "68": "CZE", + "3": "DNK", + "82": "EST", + "18": "FIN", + "4": "FRA", + "5": "DEU", + "40": "GRC", + "75": "HUN", + "20": "ISL", + "21": "IRL", + "546": "ISR", + "6": "ITA", + "701": "JPN", + "742": "KOR", + "83": "LVA", + "84": "LTU", + "22": "LUX", + "358": "MEX", + "7": "NLD", + "820": "NZL", + "8": "NOR", + "76": "POL", + "9": "PRT", + "69": "SVK", + "61": "SVN", + "50": "ESP", + "10": "SWE", + "11": "CHE", + "55": "TUR", + "12": "GBR", + "302": "USA", + "625": "AFG", + "71": "ALB", + "130": "DZA", + "225": "AGO", + "376": "AIA", + "377": "ATG", + "425": "ARG", + "610": "ARM", + "373": "ABW", + "611": "AZE", + "328": "BHS", + "530": "BHR", + "666": "BGD", + "329": "BRB", + "86": "BLR", + "352": "BLZ", + "236": "BEN", + "331": "BMU", + "630": "BTN", + "428": "BOL", + "64": "BIH", + "227": "BWA", + "431": "BRA", + "388": "VGB", + "725": "BRN", + "72": "BGR", + "287": "BFA", + "228": "BDI", + "230": "CPV", + "728": "KHM", + "229": "CMR", + "386": "CYM", + "231": "CAF", + "232": "TCD", + "730": "CHN", + "233": "COM", + "234": "COG", + "831": "COK", + "247": "CIV", + "62": "HRV", + "338": "CUB", + "30": "CYP", + "740": "PRK", + "235": "COD", + "274": "DJI", + "378": "DMA", + "340": "DOM", + "440": "ECU", + "142": "EGY", + "342": "SLV", + "245": "GNQ", + "271": "ERI", + "280": "SWZ", + "238": "ETH", + "443": "FLK", + "832": "FJI", + "840": "PYF", + "239": "GAB", + "240": "GMB", + "612": "GEO", + "241": "GHA", + "35": "GIB", + "381": "GRD", + "347": "GTM", + "243": "GIN", + "244": "GNB", + "446": "GUY", + "349": "HTI", + "351": "HND", + "735": "HKG", + "645": "IND", + "738": "IDN", + "540": "IRN", + "543": "IRQ", + "354": "JAM", + "549": "JOR", + "613": "KAZ", + "248": "KEN", + "836": "KIR", + "57": "XKV", + "552": "KWT", + "614": "KGZ", + "745": "LAO", + "555": "LBN", + "249": "LSO", + "251": "LBR", + "133": "LBY", + "70": "LIE", + "748": "MAC", + "252": "MDG", + "253": "MWI", + "751": "MYS", + "655": "MDV", + "255": "MLI", + "45": "MLT", + "859": "MHL", + "256": "MRT", + "257": "MUS", + "258": "MYT", + "860": "FSM", + "93": "MDA", + "26": "MCO", + "753": "MNG", + "65": "MNE", + "385": "MSR", + "136": "MAR", + "259": "MOZ", + "635": "MMR", + "275": "NAM", + "845": "NRU", + "660": "NPL", + "850": "NCL", + "364": "NIC", + "260": "NER", + "261": "NGA", + "856": "NIU", + "66": "MKD", + "858": "MNP", + "558": "OMN", + "665": "PAK", + "861": "PLW", + "550": "PSE", + "366": "PAN", + "862": "PNG", + "451": "PRY", + "454": "PER", + "755": "PHL", + "561": "QAT", + "77": "ROU", + "87": "RUS", + "266": "RWA", + "276": "SHN", + "382": "KNA", + "383": "LCA", + "384": "VCT", + "880": "WSM", + "268": "STP", + "566": "SAU", + "269": "SEN", + "63": "SRB", + "270": "SYC", + "272": "SLE", + "761": "SGP", + "866": "SLB", + "273": "SOM", + "218": "ZAF", + "279": "SSD", + "640": "LKA", + "278": "SDN", + "457": "SUR", + "573": "SYR", + "732": "TWN", + "615": "TJK", + "282": "TZA", + "764": "THA", + "765": "TLS", + "283": "TGO", + "868": "TKL", + "870": "TON", + "375": "TTO", + "139": "TUN", + "616": "TKM", + "387": "TCA", + "872": "TUV", + "285": "UGA", + "85": "UKR", + "576": "ARE", + "460": "URY", + "617": "UZB", + "854": "VUT", + "463": "VEN", + "769": "VNM", + "876": "WLF", + "580": "YEM", + "288": "ZMB", + "265": "ZWE", + "10040": "ACP", + "10202": "SIDS", + "10203": "FSCAC", + "10004": "A", + "10056": "A2", + "10055": "A3", + "10054": "A4", + "10006": "A7", + "10010": "E", + "10001": "F", + "10050": "F3", + "10051": "F5", + "10052": "F7", + "10053": "F8", + "10011": "F97", + "10012": "O", + "10057": "O7", + "10058": "O8", + "10059": "O9", + "10007": "S", + "20005": "ALLD", + "20002": "ALLM", + "10200": "ALLR", + "10150": "ALLMR", + "20918": "4EU001", + "917": "4EU002", + "918": "4EU003", + "919": "4EU004", + "927": "4EU999", + "20013": "5IMF0", + "907": "5IMF01", + "958": "5IMF02", + "20022": "5RDB0", + "20016": "5AFDB0", + "913": "5AFDB001", + "914": "5AFDB002", + "20015": "5ASDB0", + "915": "5ASDB01", + "916": "5ASDB02", + "20014": "5IDB0", + "909": "5IDB001", + "912": "5IDB002", + "1019": "5IDB003", + "1024": "5RDB001", + "981": "5RDB002", + "910": "5RDB003", + "906": "5RDB004", + "1013": "5RDB005", + "1015": "5RDB006", + "990": "5RDB007", + "1037": "5RDB008", + "976": "5RDB009", + "1044": "5RDB010", + "1045": "5RDB011", + "816": "5RDB999", + "20021": "1UN0", + "1020": "1UN001", + "1047": "1UN002", + "932": "1UN003", + "988": "1UN004", + "944": "1UN005", + "940": "1UN006", + "1048": "1UN007", + "1039": "1UN008", + "965": "1UN009", + "1038": "1UN010", + "923": "1UN011", + "1046": "1UN012", + "971": "1UN013", + "959": "1UN014", + "948": "1UN015", + "807": "1UN016", + "974": "1UN017", + "967": "1UN018", + "963": "1UN019", + "962": "1UN020", + "1406": "1UN021", + "964": "1UN022", + "960": "1UN023", + "966": "1UN024", + "1050": "1UN025", + "928": "1UN026", + "1023": "1UN027", + "1404": "1UN028", + "1401": "1UN029", + "975": "1UN999", + "20012": "5WBG0", + "20033": "5WB0", + "901": "5WB001", + "905": "5WB002", + "904": "5WB003", + "902": "5WB004", + "903": "5WBG002", + "900": "5WBG003", + "20034": "9OTH0", + "1012": "9OTH001", + "953": "9OTH002", + "921": "9OTH003", + "1053": "9OTH004", + "1025": "9OTH005", + "1011": "9OTH006", + "1018": "9OTH010", + "1041": "9OTH008", + "1311": "9OTH009", + "811": "9OTH011", + "1312": "9OTH012", + "1313": "9OTH013", + "997": "9OTH014", + "1016": "9OTH015", + "1052": "9OTH016", + "1049": "9OTH017", + "812": "9OTH018", + "104": "9OTH019", + "951": "9OTH020", + "978": "9OTH021", + "1014": "9OTH022", + "1054": "9OTH023", + "989": "9OTH998", + "20018": "9OTH999", + "21600": "9PRIV0", + "1637": "9PRIV1637", + "1625": "9PRIV1625", + "1609": "9PRIV1609", + "1646": "9PRIV1646", + "1601": "9PRIV1601", + "1640": "9PRIV1640", + "1620": "9PRIV1620", + "1645": "9PRIV1645", + "1614": "9PRIV1614", + "1615": "9PRIV1615", + "1632": "9PRIV1632", + "1617": "9PRIV1617", + "1618": "9PRIV1618", + "1644": "9PRIV1644", + "1627": "9PRIV1627", + "1616": "9PRIV1616", + "1626": "9PRIV1626", + "1612": "9PRIV1612", + "1642": "9PRIV1642", + "1608": "9PRIV1608", + "1636": "9PRIV1636", + "1619": "9PRIV1619", + "1639": "9PRIV1639", + "1613": "9PRIV1613", + "1633": "9PRIV1633", + "1648": "9PRIV1648", + "1638": "9PRIV1638", + "1606": "9PRIV1606", + "1610": "9PRIV1610", + "1631": "9PRIV1631", + "1605": "9PRIV1605", + "1621": "9PRIV1621", + "1611": "9PRIV1611", + "1622": "9PRIV1622", + "1643": "9PRIV1643", + "1623": "9PRIV1623", + "1641": "9PRIV1641", + "1629": "9PRIV1629", + "1628": "9PRIV1628", + "1624": "9PRIV1624", + "1630": "9PRIV1630", + "20035": "9PLG0", + "1602": "9PLG0_NL", + "1647": "9PLG0_DE", + "1634": "9PLG0_NO", + "1604": "9PLG0_GB", + "1603": "9PLG0_SE", + "20001": "DAC", + "20003": "G7", + "20004": "DACEU_EC", + "20006": "WXDAC", + "20007": "W_O", + "20011": "DACEU", + "60": "YUG_F", + "88": "SFRY_X", + "89": "E_X", + "298": "F_X", + "361": "SXM", + "489": "A7_X", + "498": "A_X", + "789": "S97_X", + "798": "S_X", + "889": "O_X", + "9998": "DPGC_X", + "10002": "F4", + "10003": "F6", + "10005": "A5", + "10016": "LDC", + "10017": "OLIC", + "10018": "LMIC", + "10019": "UMIC", + "10024": "INC_X", + "10025": "MADCT", + "10030": "HIPC", + "10045": "OLICWB", + "10046": "LMICWB", + "10047": "UMICWB", + "10048": "HICSWB", + "10049": "INCWB_X", + "10100": "DPGC", + "10201": "LLDC", + "189": "F4_X", + "289": "F6_X", + "389": "A5_X", + "589": "F97_X", + "619": "S4_X", + "689": "S4_S7_X", + "1027": "F3_X", + "1029": "F7_X", + "1030": "F8_X", + "1031": "A4_X", + "1032": "A3_X", + "10008": "S97", + "10009": "S4_S7", + "1055": "9OTH024", + "(.*)": "\\1" +} diff --git a/src/oda_reader/schemas/multisystem_translation.py b/src/oda_reader/schemas/multisystem_translation.py index 8ffd5be..a1933f9 100644 --- a/src/oda_reader/schemas/multisystem_translation.py +++ b/src/oda_reader/schemas/multisystem_translation.py @@ -2,7 +2,7 @@ from oda_reader.schemas.dac1_translation import prices_mapping from oda_reader.schemas.dac2_translation import area_code_mapping -from oda_reader.schemas.schema_tools import map_area_codes, map_amount_type_codes +from oda_reader.schemas.schema_tools import map_amount_type_codes, map_area_codes def convert_multisystem_to_dotstat_codes(df: pd.DataFrame) -> pd.DataFrame: diff --git a/src/oda_reader/schemas/schema_tools.py b/src/oda_reader/schemas/schema_tools.py index 9d1c9ed..c6170bd 100644 --- a/src/oda_reader/schemas/schema_tools.py +++ b/src/oda_reader/schemas/schema_tools.py @@ -2,7 +2,7 @@ import pandas as pd -from oda_reader.common import logger, ImporterPaths +from oda_reader.common import ImporterPaths, logger def read_schema_translation(version: str = "dac1") -> dict: @@ -17,13 +17,10 @@ def read_schema_translation(version: str = "dac1") -> dict: """ logger.info(f"Reading the {version} schema translation") - if version == "aidData": - schema = "schema" - else: - schema = "dotstat" + schema = "schema" if version == "aidData" else "dotstat" # Load the schema translation - with open(ImporterPaths.mappings / f"{version}_{schema}.json", "r") as f: + with open(ImporterPaths.mappings / f"{version}_{schema}.json") as f: mapping = json.load(f) return mapping diff --git a/src/oda_reader/schemas/xml_tools.py b/src/oda_reader/schemas/xml_tools.py index 4682878..f9c677b 100644 --- a/src/oda_reader/schemas/xml_tools.py +++ b/src/oda_reader/schemas/xml_tools.py @@ -4,7 +4,7 @@ import requests -from oda_reader.common import logger, API_RATE_LIMITER +from oda_reader.common import API_RATE_LIMITER, logger def download_xml(xml_url: str) -> requests.models.Response: @@ -172,7 +172,7 @@ def read_mapping(mapping_path: str, keys_as_int: bool, update: callable) -> dict logger.info("Not found, downloading.") update() - with open(mapping_path, "r") as f: + with open(mapping_path) as f: mapping = json.load(f) # Convert keys to integers (if required) diff --git a/src/oda_reader/tools.py b/src/oda_reader/tools.py index 311487e..7492cb9 100644 --- a/src/oda_reader/tools.py +++ b/src/oda_reader/tools.py @@ -15,9 +15,10 @@ def get_available_filters(source: str, quiet: bool = False) -> dict: Returns: dict: The available filters. """ - from oda_reader import QueryBuilder as qb from pprint import pprint + from oda_reader import QueryBuilder as qb + match source: case "dac1": f = qb.build_dac1_filter.__annotations__ diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..da013c5 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,6 @@ +.pytest_cache/ +__pycache__/ +**/__pycache__/ +.coverage +htmlcov/ +*.pyc diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..236304d --- /dev/null +++ b/tests/README.md @@ -0,0 +1,388 @@ +# ODA Reader Test Suite + +This directory contains the test suite for the ODA Reader package. + +## Structure + +``` +tests/ +├── common/ # Core utilities (rate limiter, cache) +│ └── unit/ # Unit tests for common functionality +├── download/ # Download layer (query builder, API calls) +│ └── unit/ # Unit tests for download tools +├── schemas/ # Schema translation +│ └── unit/ # Unit tests for schema translation +├── datasets/ # Dataset-specific tests +│ ├── dac1/ # DAC1 dataset tests +│ ├── dac2a/ # DAC2a dataset tests +│ └── crs/ # CRS dataset tests +│ └── integration/ # End-to-end integration tests +├── fixtures/ # Test data and mock responses +│ ├── api_responses/ +│ └── expected_outputs/ +├── conftest.py # Shared fixtures +└── utils.py # Test utilities +``` + +Pytest configuration is in `pyproject.toml` under `[tool.pytest.ini_options]`. + +## Running Tests + +### Run all unit tests (default, fast) +```bash +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 "" +``` + +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 + +# Run a specific test function +uv run pytest tests/common/unit/test_rate_limiter.py::TestRateLimiterBlocking::test_blocks_when_limit_exceeded -v +``` + +### Run with coverage +```bash +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" +``` + +This uses all available CPU cores to run unit tests in parallel for faster execution. + +## Test Markers + +Tests are organized using pytest markers to control execution: + +- `@pytest.mark.unit` - Fast unit tests with mocked dependencies (default) +- `@pytest.mark.integration` - Tests that call real OECD API (skipped by default) +- `@pytest.mark.slow` - Long-running tests (bulk downloads, extensive processing) +- `@pytest.mark.cache` - Tests that verify cache behavior + +### Using Markers + +```python +import pytest + +@pytest.mark.unit +def test_query_builder(): + """Fast unit test with no external dependencies.""" + pass + +@pytest.mark.integration +def test_real_api_call(): + """Integration test that hits real OECD API.""" + pass +``` + +## Writing Tests + +### Unit Tests + +Unit tests should: +- Mock external dependencies (HTTP calls, file I/O) +- 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 + +@pytest.mark.unit +def test_query_builder_filter(mocker): + """Test query builder creates correct filter strings.""" + from oda_reader.download.query_builder import QueryBuilder + + qb = QueryBuilder(dataflow_id="DF_DAC1", api_version=1) + result = qb.build_dac1_filter(donor="1", measure="2010") + + assert result == "1.2010....." +``` + +### 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 +- Respect rate limits +- Enable HTTP caching to avoid redundant API calls +- Test critical user-facing functionality + +Example: +```python +import pytest +from oda_reader import dac1, enable_http_cache + +@pytest.mark.integration +def test_dac1_basic_query(): + """Test basic DAC1 query returns valid data.""" + enable_http_cache() + + df = dac1( + donor="1", + start_period="2023", + end_period="2023" + ) + + assert df is not None + assert len(df) > 0 + assert "TIME_PERIOD" in df.columns + assert "OBS_VALUE" in df.columns +``` + +### Parametrized Tests + +Use parametrization to test multiple scenarios with one test function: + +```python +import pytest + +@pytest.mark.unit +@pytest.mark.parametrize("input_val,expected", [ + ("1", "1"), + (["1", "2"], "1+2"), + (None, ""), +]) +def test_filter_conversion(input_val, expected): + """Test filter string conversion handles various inputs.""" + from oda_reader.download.query_builder import QueryBuilder + + qb = QueryBuilder(dataflow_id="DF_DAC1", api_version=1) + result = qb._to_filter_str(input_val) + + assert result == expected +``` + +## Fixtures + +Key fixtures available in all tests (defined in `conftest.py`): + +### `temp_cache_dir` +Creates a temporary cache directory for testing cache behavior. + +```python +def test_cache_behavior(temp_cache_dir): + """Test uses isolated cache directory.""" + assert temp_cache_dir.exists() + # Cache operations here won't affect real cache +``` + +### `rate_limiter_fast` +Provides a fast rate limiter for testing (2 calls per 0.5 seconds). + +```python +def test_rate_limiting(rate_limiter_fast): + """Test with faster rate limiter for quick tests.""" + limiter = rate_limiter_fast + assert limiter.max_calls == 2 + assert limiter.period == 0.5 +``` + +### `sample_csv_response` +Returns sample CSV data for mocking API responses. + +```python +def test_csv_parsing(sample_csv_response): + """Test CSV parsing with sample data.""" + assert "DONOR,RECIPIENT" in sample_csv_response +``` + +### `fixtures_dir` +Returns the path to the fixtures directory. + +```python +def test_load_fixture(fixtures_dir): + """Test loading fixture files.""" + fixture_path = fixtures_dir / "api_responses" / "sample.json" + # Load and use fixture +``` + +### Auto-enabled Fixtures + +The `disable_cache_for_tests` fixture runs automatically for all tests, ensuring the HTTP cache is disabled by default to provide isolation between tests. + +## Test Utilities + +Helper functions are available in `tests/utils.py`: + +### `assert_dataframe_schema(df, expected_columns)` +Validates DataFrame has expected columns and types. + +```python +from tests.utils import assert_dataframe_schema + +def test_dataframe_structure(): + df = get_some_dataframe() + assert_dataframe_schema(df, { + "TIME_PERIOD": "int64", + "OBS_VALUE": "float64", + }) +``` + +### `load_json_fixture(fixtures_dir, fixture_name)` +Loads a JSON fixture file. + +```python +from tests.utils import load_json_fixture + +def test_with_fixture(fixtures_dir): + data = load_json_fixture(fixtures_dir, "sample_response") + assert data["status"] == "success" +``` + +### `mock_api_response(status_code, text, from_cache=False)` +Creates a mock API response tuple. + +```python +from tests.utils import mock_api_response + +def test_api_error_handling(mocker): + mocker.patch( + "oda_reader.common._get_response_text", + return_value=mock_api_response(404, "Not found") + ) + # Test error handling +``` + +## CI/CD + +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 + +### Running Locally Before Pushing + +```bash +# Quick check (unit tests only) +uv run pytest tests/ -v + +# Full check (includes integration) +uv run pytest tests/ -m "" -v + +# Lint check +uv run ruff check src/oda_reader/ tests/ +uv run ruff format --check src/oda_reader/ tests/ +``` + +## Test Coverage + +To generate a coverage report: + +```bash +# Terminal report +uv run pytest tests/ --cov=src/oda_reader --cov-report=term + +# HTML report (opens in browser) +uv run pytest tests/ --cov=src/oda_reader --cov-report=html +open htmlcov/index.html +``` + +Coverage goals: +- Overall: >80% +- Core modules (common, download, query_builder): >90% +- Dataset modules (dac1, dac2a, crs): >70% + +## 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 + +## 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 + +## 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 + +Example workflow: +```bash +# Create test file +touch tests/download/unit/test_new_feature.py + +# Write failing test +# (edit file) + +# Run test to see it fail +uv run pytest tests/download/unit/test_new_feature.py -v + +# Implement feature +# (edit source file) + +# Run test to see it pass +uv run pytest tests/download/unit/test_new_feature.py -v + +# Run full unit suite to ensure no regressions +uv run pytest tests/ -v +``` diff --git a/tests/common/unit/test_cache.py b/tests/common/unit/test_cache.py new file mode 100644 index 0000000..45c30a9 --- /dev/null +++ b/tests/common/unit/test_cache.py @@ -0,0 +1,64 @@ +"""Unit tests for HTTP caching functionality.""" + +import pytest + +from oda_reader import ( + clear_http_cache, + common, + disable_http_cache, + enable_http_cache, + get_http_cache_info, +) + + +@pytest.mark.unit +@pytest.mark.cache +@pytest.mark.xdist_group("cache") +class TestHTTPCache: + """Test HTTP cache control functions. + + Note: These tests are grouped to run serially (not in parallel) + because they test global cache state that can't be safely shared + across pytest-xdist workers. + """ + + def test_disable_cache_sets_flag(self): + """Test that disable_http_cache sets the flag.""" + enable_http_cache() + assert common._CACHE_ENABLED is True + + disable_http_cache() + + # Check the global variable through the module + assert common._CACHE_ENABLED is False + + # Cleanup + enable_http_cache() + + def test_enable_cache_sets_flag(self): + """Test that enable_http_cache sets the flag.""" + disable_http_cache() + + enable_http_cache() + + assert common._CACHE_ENABLED is True + + def test_clear_cache_resets_counters(self): + """Test that clear_http_cache resets cache statistics.""" + enable_http_cache() + clear_http_cache() + + info = get_http_cache_info() + + assert info["response_count"] == 0 + assert info["redirects_count"] == 0 + + def test_get_cache_info_returns_dict(self): + """Test that get_http_cache_info returns expected structure.""" + enable_http_cache() + + info = get_http_cache_info() + + assert isinstance(info, dict) + assert "response_count" in info + assert "redirects_count" in info diff --git a/tests/common/unit/test_rate_limiter.py b/tests/common/unit/test_rate_limiter.py new file mode 100644 index 0000000..55500a3 --- /dev/null +++ b/tests/common/unit/test_rate_limiter.py @@ -0,0 +1,56 @@ +"""Unit tests for RateLimiter class.""" + +import time + +import pytest + +from oda_reader.common import RateLimiter + + +@pytest.mark.unit +class TestRateLimiterBlocking: + """Test rate limiter blocking behavior.""" + + def test_blocks_when_limit_exceeded(self): + """Verify rate limiter blocks when limit is reached.""" + limiter = RateLimiter(max_calls=2, period=1.0) + + start = time.time() + for _ in range(3): + limiter.wait() + elapsed = time.time() - start + + # Third call should block for approximately 1 second + assert elapsed >= 1.0, f"Expected blocking >= 1.0s, got {elapsed:.2f}s" + assert elapsed < 1.2, f"Blocking took too long: {elapsed:.2f}s" + + def test_no_blocking_within_limit(self): + """Verify rate limiter doesn't block when within limit.""" + limiter = RateLimiter(max_calls=5, period=1.0) + + start = time.time() + for _ in range(3): + limiter.wait() + elapsed = time.time() - start + + # Should complete almost immediately + assert elapsed < 0.1, f"Unexpected blocking: {elapsed:.2f}s" + + def test_configuration_changes_take_effect(self): + """Verify changing rate limiter configuration works.""" + limiter = RateLimiter(max_calls=2, period=0.5) + + # Use up the initial limit + limiter.wait() + limiter.wait() + + # Change configuration + limiter.max_calls = 10 + limiter.period = 0.5 + + # Should not block now + start = time.time() + limiter.wait() + elapsed = time.time() - start + + assert elapsed < 0.1, "Should not block after increasing limit" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..1670d6b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,66 @@ +"""Shared pytest fixtures and configuration.""" + +from pathlib import Path + +import pytest + +from oda_reader import disable_http_cache, enable_http_cache +from oda_reader.common import RateLimiter + + +@pytest.fixture(autouse=True) +def disable_cache_for_tests(): + """Disable HTTP cache for all tests by default.""" + disable_http_cache() + yield + enable_http_cache() + + +@pytest.fixture +def temp_cache_dir(tmp_path, monkeypatch): + """Create and configure a temporary cache directory for testing. + + Args: + tmp_path: pytest's temporary directory fixture + monkeypatch: pytest's monkeypatch fixture + + Yields: + Path: Path to the temporary cache directory + """ + cache_dir = tmp_path / "test_cache" + cache_dir.mkdir() + monkeypatch.setenv("ODA_READER_CACHE_DIR", str(cache_dir)) + yield cache_dir + + +@pytest.fixture +def rate_limiter_fast(): + """Create a fast rate limiter for testing (2 calls per 0.5 seconds). + + Returns: + RateLimiter: Configured rate limiter for testing + """ + return RateLimiter(max_calls=2, period=0.5) + + +@pytest.fixture +def sample_csv_response(): + """Return a sample CSV response string for mocking API responses. + + Returns: + str: Sample CSV data as string + """ + return """DONOR,RECIPIENT,TIME_PERIOD,OBS_VALUE +1,503,2023,1000.5 +1,503,2024,1500.75 +""" + + +@pytest.fixture +def fixtures_dir(): + """Return the path to the fixtures directory. + + Returns: + Path: Path to tests/fixtures/ + """ + return Path(__file__).parent / "fixtures" diff --git a/tests/datasets/crs/integration/test_crs_e2e.py b/tests/datasets/crs/integration/test_crs_e2e.py new file mode 100644 index 0000000..4199666 --- /dev/null +++ b/tests/datasets/crs/integration/test_crs_e2e.py @@ -0,0 +1,57 @@ +"""Integration tests for CRS dataset.""" + +import pytest + +from oda_reader import download_crs, enable_http_cache + + +@pytest.mark.integration +@pytest.mark.slow +class TestCRSIntegration: + """End-to-end tests for CRS with real API.""" + + def test_crs_microdata_query(self): + """Test CRS microdata query returns project-level data.""" + enable_http_cache() + + # Small query: US education projects (microdata) + # Using pre_process=False and dotstat_codes=False to test raw API + df = download_crs( + start_year=2023, + end_year=2023, + filters={"donor": "USA", "sector": "110"}, # US, Education sector + pre_process=False, + dotstat_codes=False, + ) + + assert df is not None + assert len(df) > 0 + # Raw API columns + assert "TIME_PERIOD" in df.columns + assert "OBS_VALUE" in df.columns + + def test_crs_aggregated_query(self): + """Test CRS aggregated query returns summary data.""" + enable_http_cache() + + # Semi-aggregated data (matches Data Explorer format) + # Using pre_process=False and dotstat_codes=False to test raw API + df = download_crs( + start_year=2023, + end_year=2023, + filters={ + "donor": "USA", + "recipient": "NGA", # Nigeria + "microdata": False, + "channel": "_T", # Total across channels + "modality": "_T", # Total across modalities + }, + pre_process=False, + dotstat_codes=False, + ) + + assert df is not None + assert len(df) > 0 + # Raw API columns + assert "TIME_PERIOD" in df.columns + assert "OBS_VALUE" in df.columns diff --git a/tests/datasets/dac1/integration/test_dac1_e2e.py b/tests/datasets/dac1/integration/test_dac1_e2e.py new file mode 100644 index 0000000..7629b9e --- /dev/null +++ b/tests/datasets/dac1/integration/test_dac1_e2e.py @@ -0,0 +1,68 @@ +"""Integration tests for DAC1 dataset.""" + +import pytest + +from oda_reader import download_dac1, enable_http_cache + + +@pytest.mark.integration +class TestDAC1Integration: + """End-to-end tests for DAC1 with real API.""" + + def test_basic_query_returns_dataframe(self): + """Test basic DAC1 query returns valid DataFrame.""" + enable_http_cache() + + # Very small query: just one year, no specific filters + # Using pre_process=False and dotstat_codes=False to test raw API + df = download_dac1( + start_year=2023, + end_year=2023, + pre_process=False, + dotstat_codes=False, + ) + + assert df is not None + assert len(df) > 0 + + # Raw API columns + assert "TIME_PERIOD" in df.columns + assert "OBS_VALUE" in df.columns + + def test_query_with_preprocessing(self): + """Test DAC1 query with preprocessing enabled.""" + enable_http_cache() + + # Test with preprocessing - should rename columns to .stat schema + df = download_dac1( + start_year=2023, + end_year=2023, + pre_process=True, + dotstat_codes=False, # Only test preprocessing, not code translation + ) + + assert df is not None + assert len(df) > 0 + + # With preprocessing, TIME_PERIOD → year, OBS_VALUE → value + assert "year" in df.columns + assert "value" in df.columns + + def test_query_with_dotstat_codes(self): + """Test DAC1 query with legacy .stat code translation.""" + enable_http_cache() + + # Test with both preprocessing and code translation + df = download_dac1( + start_year=2023, + end_year=2023, + pre_process=True, + dotstat_codes=True, + ) + + assert df is not None + assert len(df) > 0 + + # With preprocessing enabled, columns are renamed + assert "year" in df.columns + assert "value" in df.columns diff --git a/tests/datasets/dac2a/integration/test_dac2a_e2e.py b/tests/datasets/dac2a/integration/test_dac2a_e2e.py new file mode 100644 index 0000000..cc27489 --- /dev/null +++ b/tests/datasets/dac2a/integration/test_dac2a_e2e.py @@ -0,0 +1,30 @@ +"""Integration tests for DAC2a dataset.""" + +import pytest + +from oda_reader import download_dac2a, enable_http_cache + + +@pytest.mark.integration +class TestDAC2aIntegration: + """End-to-end tests for DAC2a with real API.""" + + def test_basic_query(self): + """Test basic DAC2a query returns valid DataFrame.""" + enable_http_cache() + + # Small query: US bilateral flows to Kenya + # Using pre_process=False and dotstat_codes=False to test raw API + df = download_dac2a( + start_year=2023, + end_year=2023, + filters={"donor": "USA", "recipient": "KEN"}, # US to Kenya + pre_process=False, + dotstat_codes=False, + ) + + assert df is not None + assert len(df) > 0 + # Raw API columns + assert "TIME_PERIOD" in df.columns + assert "OBS_VALUE" in df.columns diff --git a/tests/download/unit/test_download_tools.py b/tests/download/unit/test_download_tools.py new file mode 100644 index 0000000..ca16d32 --- /dev/null +++ b/tests/download/unit/test_download_tools.py @@ -0,0 +1,57 @@ +"""Unit tests for download tools with mocked API responses.""" + +import pytest + +from oda_reader.common import get_data_from_api + + +@pytest.mark.unit +class TestDownloadWithMocks: + """Test download functions with mocked HTTP responses.""" + + def test_get_data_from_api_success(self, mocker, sample_csv_response): + """Test successful API data retrieval.""" + # Mock the _get_response_text function + mock_response = (200, sample_csv_response, False) + mocker.patch( + "oda_reader.common._get_response_text", + return_value=mock_response, + ) + + result = get_data_from_api("https://example.com/data") + + assert result == sample_csv_response + assert "DONOR,RECIPIENT" in result + + def test_get_data_from_api_404_triggers_retry(self, mocker): + """Test that 404 with 'Dataflow' message triggers version fallback.""" + # First call returns 404 with Dataflow message + # Second call (with fallback version) returns success + mock_responses = [ + (404, "Dataflow not found", False), + (200, "DONOR,VALUE\n1,100", False), + ] + + mock = mocker.patch( + "oda_reader.common._get_response_text", + side_effect=mock_responses, + ) + + # URL with version 2.0 should fallback to 1.9 + url = "https://sdmx.oecd.org/public/rest/data/OECD.DCD.FSD,DF_DAC1,2.0/" + result = get_data_from_api(url) + + # Should have made 2 calls (original + fallback) + assert mock.call_count == 2 + assert result == "DONOR,VALUE\n1,100" + + def test_get_data_from_api_non_404_error_raises(self, mocker): + """Test that non-404 errors raise ConnectionError.""" + mock_response = (500, "Internal Server Error", False) + mocker.patch( + "oda_reader.common._get_response_text", + return_value=mock_response, + ) + + with pytest.raises(ConnectionError, match="Error 500"): + get_data_from_api("https://example.com/data") diff --git a/tests/download/unit/test_query_builder.py b/tests/download/unit/test_query_builder.py new file mode 100644 index 0000000..e5a9589 --- /dev/null +++ b/tests/download/unit/test_query_builder.py @@ -0,0 +1,173 @@ +"""Unit tests for QueryBuilder filter construction.""" + +import pytest + +from oda_reader.download.query_builder import QueryBuilder + + +@pytest.mark.unit +class TestQueryBuilderDAC1: + """Test DAC1 filter building.""" + + def test_dac1_filter_basic(self): + """Test basic DAC1 filter construction.""" + qb = QueryBuilder(dataflow_id="DF_DAC1", api_version=1) + result = qb.build_dac1_filter( + donor="1", + measure="2010", + flow_type="1140", + ) + + # Should have donor.measure.untied.flow_type.unit_measure.price_base.period + # With None values as empty strings for API v1 + assert result == "1.2010..1140..." + + def test_dac1_filter_all_parameters(self): + """Test DAC1 filter with all parameters specified.""" + qb = QueryBuilder(dataflow_id="DF_DAC1", api_version=1) + result = qb.build_dac1_filter( + donor="1", + measure="2010", + flow_type="1140", + unit_measure="USD", + price_base="V", + ) + + assert result == "1.2010..1140.USD.V." + + def test_dac1_filter_no_parameters(self): + """Test DAC1 filter with no parameters (all dimensions).""" + qb = QueryBuilder(dataflow_id="DF_DAC1", api_version=1) + result = qb.build_dac1_filter() + + # All empty for API v1 + assert result == "......" + + +@pytest.mark.unit +class TestQueryBuilderDAC2a: + """Test DAC2a filter building.""" + + def test_dac2a_filter_basic(self): + """Test basic DAC2a filter construction.""" + qb = QueryBuilder(dataflow_id="DF_DAC2A", api_version=1) + result = qb.build_dac2a_filter( + donor="1", + recipient="503", + measure="1010", + ) + + # donor.recipient.measure.unit_measure.price_base + assert result == "1.503.1010.." + + def test_dac2a_filter_all_parameters(self): + """Test DAC2a filter with all parameters.""" + qb = QueryBuilder(dataflow_id="DF_DAC2A", api_version=1) + result = qb.build_dac2a_filter( + donor="1", + recipient="503", + measure="1010", + unit_measure="USD", + price_base="V", + ) + + assert result == "1.503.1010.USD.V" + + +@pytest.mark.unit +class TestQueryBuilderCRS: + """Test CRS filter building.""" + + @pytest.mark.parametrize( + "microdata,expected_md_dim", + [ + (True, "DD"), + (False, "_T"), + ], + ) + def test_crs_filter_microdata_flag(self, microdata, expected_md_dim): + """Test CRS filter respects microdata flag.""" + qb = QueryBuilder(dataflow_id="DF_CRS", api_version=1) + result = qb.build_crs_filter( + donor="1", + microdata=microdata, + ) + + # Check that md_dim (9th position) matches expected + parts = result.split(".") + assert parts[8] == expected_md_dim + + def test_crs_filter_all_dimensions(self): + """Test CRS filter with all dimensions specified.""" + qb = QueryBuilder(dataflow_id="DF_CRS", api_version=1) + result = qb.build_crs_filter( + donor="1", + recipient="503", + sector="100", + measure="1010", + channel="1", + modality="M01", + flow_type="1140", + price_base="V", + unit_measure="USD", + microdata=True, + ) + + expected = "1.503.100.1010.1.M01.1140.V.DD..USD" + assert result == expected + + +@pytest.mark.unit +class TestQueryBuilderURL: + """Test complete URL construction.""" + + def test_build_query_api_v1(self): + """Test complete URL construction for API v1.""" + qb = QueryBuilder( + dataflow_id="DF_DAC1", + dataflow_version="1.0", + api_version=1, + ) + qb.set_filter("1.2010..1140...") + qb.set_time_period(start=2020, end=2023) + + url = qb.build_query() + + assert "https://sdmx.oecd.org/public/rest/data/" in url + assert "OECD.DCD.FSD" in url + assert "DF_DAC1,1.0/" in url + assert "1.2010..1140..." in url + assert "startPeriod=2020" in url + assert "endPeriod=2023" in url + assert "format=csvfilewithlabels" in url + + def test_build_query_api_v2(self): + """Test complete URL construction for API v2.""" + qb = QueryBuilder( + dataflow_id="DF_DAC1", + dataflow_version="2.0", + api_version=2, + ) + qb.set_filter("*.*.*.*.*.*.*") + qb.set_time_period(start=2020, end=2023) + + url = qb.build_query() + + assert "https://sdmx.oecd.org/public/rest/v2/data/dataflow/" in url + assert "OECD.DCD.FSD" in url + assert "DF_DAC1/2.0/" in url + assert "*.*.*.*.*.*.*" in url + assert "c[TIME_PERIOD]=ge:2020+le:2023" in url + assert "format=csvfilewithlabels" in url + + def test_build_query_crs_base_url(self): + """Test CRS uses correct base URL.""" + qb = QueryBuilder( + dataflow_id="DF_CRS", + dataflow_version="1.0", + api_version=1, + ) + + url = qb.build_query() + + assert "https://sdmx.oecd.org/dcd-public/rest/data/" in url diff --git a/tests/download/unit/test_version_fallback.py b/tests/download/unit/test_version_fallback.py new file mode 100644 index 0000000..f90828a --- /dev/null +++ b/tests/download/unit/test_version_fallback.py @@ -0,0 +1,52 @@ +"""Unit tests for dataflow version fallback logic.""" + +import pytest + +from oda_reader.common import _get_dataflow_version, _replace_dataflow_version + + +@pytest.mark.unit +class TestVersionFallback: + """Test dataflow version manipulation.""" + + @pytest.mark.parametrize( + "url,expected_version", + [ + ( + "https://sdmx.oecd.org/public/rest/data/OECD.DCD.FSD,DF_DAC1,1.0/", + "1.0", + ), + ( + "https://sdmx.oecd.org/public/rest/data/OECD.DCD.FSD,DF_DAC2A,2.5/", + "2.5", + ), + ( + "https://sdmx.oecd.org/dcd-public/rest/data/OECD.DCD.FSD,DF_CRS,3.2/", + "3.2", + ), + ], + ) + def test_get_dataflow_version(self, url, expected_version): + """Test extracting version from URL.""" + result = _get_dataflow_version(url) + assert result == expected_version + + @pytest.mark.parametrize( + "original_url,new_version,expected_url", + [ + ( + "https://sdmx.oecd.org/public/rest/data/OECD.DCD.FSD,DF_DAC1,2.0/", + "1.9", + "https://sdmx.oecd.org/public/rest/data/OECD.DCD.FSD,DF_DAC1,1.9/", + ), + ( + "https://sdmx.oecd.org/public/rest/data/OECD.DCD.FSD,DF_DAC1,2.0/filters", + "1.5", + "https://sdmx.oecd.org/public/rest/data/OECD.DCD.FSD,DF_DAC1,1.5/filters", + ), + ], + ) + def test_replace_dataflow_version(self, original_url, new_version, expected_url): + """Test replacing version in URL.""" + result = _replace_dataflow_version(original_url, new_version) + assert result == expected_url diff --git a/tests/fixtures/api_responses/.gitkeep b/tests/fixtures/api_responses/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/expected_outputs/.gitkeep b/tests/fixtures/expected_outputs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/schemas/unit/test_dac1_translation.py b/tests/schemas/unit/test_dac1_translation.py new file mode 100644 index 0000000..8519f74 --- /dev/null +++ b/tests/schemas/unit/test_dac1_translation.py @@ -0,0 +1,85 @@ +"""Unit tests for DAC1 schema translation.""" + +import pytest + +from oda_reader.schemas.dac1_translation import area_code_mapping + + +@pytest.mark.unit +class TestDAC1Translation: + """Test DAC1 code translations.""" + + def test_area_code_mapping_returns_dict(self): + """Test that area_code_mapping returns a dictionary.""" + result = area_code_mapping() + + assert isinstance(result, dict) + assert len(result) > 0 + + def test_area_code_mapping_has_expected_size(self): + """Test that area_code_mapping returns expected number of mappings.""" + result = area_code_mapping() + + # As of 2025, there should be hundreds of area codes + assert len(result) > 400, f"Expected >400 mappings, got {len(result)}" + + @pytest.mark.parametrize( + "new_code,expected_old_code", + [ + # DAC members (donors) + (801, "AUS"), # Australia + (1, "AUT"), # Austria + (2, "BEL"), # Belgium + (301, "CAN"), # Canada + (3, "DNK"), # Denmark + (4, "FRA"), # France + (5, "DEU"), # Germany + (18, "FIN"), # Finland + (40, "GRC"), # Greece + (75, "HUN"), # Hungary + (742, "KOR"), # Korea + (22, "LUX"), # Luxembourg + (358, "MEX"), # Mexico + # Additional codes + (434, "CHL"), # Chile + (437, "COL"), # Colombia + (336, "CRI"), # Costa Rica + (68, "CZE"), # Czech Republic + (82, "EST"), # Estonia + (83, "LVA"), # Latvia + (84, "LTU"), # Lithuania + ], + ) + def test_donor_code_translation_spot_check(self, new_code, expected_old_code): + """Spot check critical donor code translations.""" + mapping = area_code_mapping() + + assert new_code in mapping, f"Code {new_code} not found in mapping" + assert mapping[new_code] == expected_old_code, ( + f"Expected {new_code} -> {expected_old_code}, " + f"got {new_code} -> {mapping[new_code]}" + ) + + def test_area_code_mapping_keys_are_integers(self): + """Test that area_code_mapping keys are integers.""" + result = area_code_mapping() + + for key in result: + assert isinstance(key, int), f"Key {key} is not an integer, got {type(key)}" + + def test_area_code_mapping_values_are_strings(self): + """Test that area_code_mapping values are strings.""" + result = area_code_mapping() + + for value in result.values(): + assert isinstance( + value, str + ), f"Value {value} is not a string, got {type(value)}" + + def test_area_code_mapping_no_empty_values(self): + """Test that area_code_mapping has no empty values.""" + result = area_code_mapping() + + for key, value in result.items(): + assert value, f"Key {key} has empty value" + assert len(value) > 0, f"Key {key} has empty string value" diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..2b97f77 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,53 @@ +"""Test utilities and helper functions.""" + +import json +from pathlib import Path +from typing import Any + +import pandas as pd + + +def assert_dataframe_schema(df: pd.DataFrame, expected_columns: dict[str, str]) -> None: + """Validate DataFrame has expected columns and types. + + Args: + df: DataFrame to validate + expected_columns: Dict mapping column names to expected dtypes + + Raises: + AssertionError: If schema doesn't match expectations + """ + for col, dtype in expected_columns.items(): + assert col in df.columns, f"Column '{col}' not found in DataFrame" + assert ( + str(df[col].dtype) == dtype + ), f"Column '{col}' has dtype {df[col].dtype}, expected {dtype}" + + +def load_json_fixture(fixtures_dir: Path, fixture_name: str) -> Any: + """Load a JSON fixture file. + + Args: + fixtures_dir: Path to fixtures directory + fixture_name: Name of the fixture file (without .json extension) + + Returns: + Parsed JSON data + """ + fixture_path = fixtures_dir / "api_responses" / f"{fixture_name}.json" + with open(fixture_path) as f: + return json.load(f) + + +def mock_api_response(status_code: int, text: str, from_cache: bool = False) -> tuple: + """Create a mock API response tuple. + + Args: + status_code: HTTP status code + text: Response text + from_cache: Whether response is from cache + + Returns: + tuple: (status_code, text, from_cache) matching _get_response_text return + """ + return (status_code, text, from_cache) diff --git a/uv.lock b/uv.lock index b33cf76..531fd49 100644 --- a/uv.lock +++ b/uv.lock @@ -16,6 +16,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "backrefs" +version = "5.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/a7/312f673df6a79003279e1f55619abbe7daebbb87c17c976ddc0345c04c7b/backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59", size = 5765857, upload-time = "2025-06-22T19:34:13.97Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/4d/798dc1f30468134906575156c089c492cf79b5a5fd373f07fe26c4d046bf/backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f", size = 380267, upload-time = "2025-06-22T19:34:05.252Z" }, + { url = "https://files.pythonhosted.org/packages/55/07/f0b3375bf0d06014e9787797e6b7cc02b38ac9ff9726ccfe834d94e9991e/backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf", size = 392072, upload-time = "2025-06-22T19:34:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/9d/12/4f345407259dd60a0997107758ba3f221cf89a9b5a0f8ed5b961aef97253/backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa", size = 397947, upload-time = "2025-06-22T19:34:08.172Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/fa31834dc27a7f05e5290eae47c82690edc3a7b37d58f7fb35a1bdbf355b/backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b", size = 399843, upload-time = "2025-06-22T19:34:09.68Z" }, + { url = "https://files.pythonhosted.org/packages/fc/24/b29af34b2c9c41645a9f4ff117bae860291780d73880f449e0b5d948c070/backrefs-5.9-py314-none-any.whl", hash = "sha256:df5e169836cc8acb5e440ebae9aad4bf9d15e226d3bad049cf3f6a5c20cc8dc9", size = 411762, upload-time = "2025-06-22T19:34:11.037Z" }, + { url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" }, +] + [[package]] name = "cattrs" version = "25.3.0" @@ -39,6 +62,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, ] +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.4" @@ -128,6 +160,140 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/38/ee22495420457259d2f3390309505ea98f98a5eed40901cf62196abad006/coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050", size = 811905, upload-time = "2025-10-15T15:15:08.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/95/c49df0aceb5507a80b9fe5172d3d39bf23f05be40c23c8d77d556df96cec/coverage-7.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb53f1e8adeeb2e78962bade0c08bfdc461853c7969706ed901821e009b35e31", size = 215800, upload-time = "2025-10-15T15:12:19.824Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c6/7bb46ce01ed634fff1d7bb53a54049f539971862cc388b304ff3c51b4f66/coverage-7.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9a03ec6cb9f40a5c360f138b88266fd8f58408d71e89f536b4f91d85721d075", size = 216198, upload-time = "2025-10-15T15:12:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/94/b2/75d9d8fbf2900268aca5de29cd0a0fe671b0f69ef88be16767cc3c828b85/coverage-7.11.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d7f0616c557cbc3d1c2090334eddcbb70e1ae3a40b07222d62b3aa47f608fab", size = 242953, upload-time = "2025-10-15T15:12:24.139Z" }, + { url = "https://files.pythonhosted.org/packages/65/ac/acaa984c18f440170525a8743eb4b6c960ace2dbad80dc22056a437fc3c6/coverage-7.11.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e44a86a47bbdf83b0a3ea4d7df5410d6b1a0de984fbd805fa5101f3624b9abe0", size = 244766, upload-time = "2025-10-15T15:12:25.974Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0d/938d0bff76dfa4a6b228c3fc4b3e1c0e2ad4aa6200c141fcda2bd1170227/coverage-7.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:596763d2f9a0ee7eec6e643e29660def2eef297e1de0d334c78c08706f1cb785", size = 246625, upload-time = "2025-10-15T15:12:27.387Z" }, + { url = "https://files.pythonhosted.org/packages/38/54/8f5f5e84bfa268df98f46b2cb396b1009734cfb1e5d6adb663d284893b32/coverage-7.11.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef55537ff511b5e0a43edb4c50a7bf7ba1c3eea20b4f49b1490f1e8e0e42c591", size = 243568, upload-time = "2025-10-15T15:12:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/68/30/8ba337c2877fe3f2e1af0ed7ff4be0c0c4aca44d6f4007040f3ca2255e99/coverage-7.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cbabd8f4d0d3dc571d77ae5bdbfa6afe5061e679a9d74b6797c48d143307088", size = 244665, upload-time = "2025-10-15T15:12:30.297Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fb/c6f1d6d9a665536b7dde2333346f0cc41dc6a60bd1ffc10cd5c33e7eb000/coverage-7.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e24045453384e0ae2a587d562df2a04d852672eb63051d16096d3f08aa4c7c2f", size = 242681, upload-time = "2025-10-15T15:12:32.326Z" }, + { url = "https://files.pythonhosted.org/packages/be/38/1b532319af5f991fa153c20373291dc65c2bf532af7dbcffdeef745c8f79/coverage-7.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:7161edd3426c8d19bdccde7d49e6f27f748f3c31cc350c5de7c633fea445d866", size = 242912, upload-time = "2025-10-15T15:12:34.079Z" }, + { url = "https://files.pythonhosted.org/packages/67/3d/f39331c60ef6050d2a861dc1b514fa78f85f792820b68e8c04196ad733d6/coverage-7.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d4ed4de17e692ba6415b0587bc7f12bc80915031fc9db46a23ce70fc88c9841", size = 243559, upload-time = "2025-10-15T15:12:35.809Z" }, + { url = "https://files.pythonhosted.org/packages/4b/55/cb7c9df9d0495036ce582a8a2958d50c23cd73f84a23284bc23bd4711a6f/coverage-7.11.0-cp310-cp310-win32.whl", hash = "sha256:765c0bc8fe46f48e341ef737c91c715bd2a53a12792592296a095f0c237e09cf", size = 218266, upload-time = "2025-10-15T15:12:37.429Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/b79cb275fa7bd0208767f89d57a1b5f6ba830813875738599741b97c2e04/coverage-7.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:24d6f3128f1b2d20d84b24f4074475457faedc3d4613a7e66b5e769939c7d969", size = 219169, upload-time = "2025-10-15T15:12:39.25Z" }, + { url = "https://files.pythonhosted.org/packages/49/3a/ee1074c15c408ddddddb1db7dd904f6b81bc524e01f5a1c5920e13dbde23/coverage-7.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d58ecaa865c5b9fa56e35efc51d1014d4c0d22838815b9fce57a27dd9576847", size = 215912, upload-time = "2025-10-15T15:12:40.665Z" }, + { url = "https://files.pythonhosted.org/packages/70/c4/9f44bebe5cb15f31608597b037d78799cc5f450044465bcd1ae8cb222fe1/coverage-7.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b679e171f1c104a5668550ada700e3c4937110dbdd153b7ef9055c4f1a1ee3cc", size = 216310, upload-time = "2025-10-15T15:12:42.461Z" }, + { url = "https://files.pythonhosted.org/packages/42/01/5e06077cfef92d8af926bdd86b84fb28bf9bc6ad27343d68be9b501d89f2/coverage-7.11.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca61691ba8c5b6797deb221a0d09d7470364733ea9c69425a640f1f01b7c5bf0", size = 246706, upload-time = "2025-10-15T15:12:44.001Z" }, + { url = "https://files.pythonhosted.org/packages/40/b8/7a3f1f33b35cc4a6c37e759137533119560d06c0cc14753d1a803be0cd4a/coverage-7.11.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aef1747ede4bd8ca9cfc04cc3011516500c6891f1b33a94add3253f6f876b7b7", size = 248634, upload-time = "2025-10-15T15:12:45.768Z" }, + { url = "https://files.pythonhosted.org/packages/7a/41/7f987eb33de386bc4c665ab0bf98d15fcf203369d6aacae74f5dd8ec489a/coverage-7.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1839d08406e4cba2953dcc0ffb312252f14d7c4c96919f70167611f4dee2623", size = 250741, upload-time = "2025-10-15T15:12:47.222Z" }, + { url = "https://files.pythonhosted.org/packages/23/c1/a4e0ca6a4e83069fb8216b49b30a7352061ca0cb38654bd2dc96b7b3b7da/coverage-7.11.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e0eb0a2dcc62478eb5b4cbb80b97bdee852d7e280b90e81f11b407d0b81c4287", size = 246837, upload-time = "2025-10-15T15:12:48.904Z" }, + { url = "https://files.pythonhosted.org/packages/5d/03/ced062a17f7c38b4728ff76c3acb40d8465634b20b4833cdb3cc3a74e115/coverage-7.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fbea96343b53f65d5351d8fd3b34fd415a2670d7c300b06d3e14a5af4f552", size = 248429, upload-time = "2025-10-15T15:12:50.73Z" }, + { url = "https://files.pythonhosted.org/packages/97/af/a7c6f194bb8c5a2705ae019036b8fe7f49ea818d638eedb15fdb7bed227c/coverage-7.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:214b622259dd0cf435f10241f1333d32caa64dbc27f8790ab693428a141723de", size = 246490, upload-time = "2025-10-15T15:12:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c3/aab4df02b04a8fde79068c3c41ad7a622b0ef2b12e1ed154da986a727c3f/coverage-7.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:258d9967520cca899695d4eb7ea38be03f06951d6ca2f21fb48b1235f791e601", size = 246208, upload-time = "2025-10-15T15:12:54.586Z" }, + { url = "https://files.pythonhosted.org/packages/30/d8/e282ec19cd658238d60ed404f99ef2e45eed52e81b866ab1518c0d4163cf/coverage-7.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cf9e6ff4ca908ca15c157c409d608da77a56a09877b97c889b98fb2c32b6465e", size = 247126, upload-time = "2025-10-15T15:12:56.485Z" }, + { url = "https://files.pythonhosted.org/packages/d1/17/a635fa07fac23adb1a5451ec756216768c2767efaed2e4331710342a3399/coverage-7.11.0-cp311-cp311-win32.whl", hash = "sha256:fcc15fc462707b0680cff6242c48625da7f9a16a28a41bb8fd7a4280920e676c", size = 218314, upload-time = "2025-10-15T15:12:58.365Z" }, + { url = "https://files.pythonhosted.org/packages/2a/29/2ac1dfcdd4ab9a70026edc8d715ece9b4be9a1653075c658ee6f271f394d/coverage-7.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:865965bf955d92790f1facd64fe7ff73551bd2c1e7e6b26443934e9701ba30b9", size = 219203, upload-time = "2025-10-15T15:12:59.902Z" }, + { url = "https://files.pythonhosted.org/packages/03/21/5ce8b3a0133179115af4c041abf2ee652395837cb896614beb8ce8ddcfd9/coverage-7.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:5693e57a065760dcbeb292d60cc4d0231a6d4b6b6f6a3191561e1d5e8820b745", size = 217879, upload-time = "2025-10-15T15:13:01.35Z" }, + { url = "https://files.pythonhosted.org/packages/c4/db/86f6906a7c7edc1a52b2c6682d6dd9be775d73c0dfe2b84f8923dfea5784/coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c49e77811cf9d024b95faf86c3f059b11c0c9be0b0d61bc598f453703bd6fd1", size = 216098, upload-time = "2025-10-15T15:13:02.916Z" }, + { url = "https://files.pythonhosted.org/packages/21/54/e7b26157048c7ba555596aad8569ff903d6cd67867d41b75287323678ede/coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a61e37a403a778e2cda2a6a39abcc895f1d984071942a41074b5c7ee31642007", size = 216331, upload-time = "2025-10-15T15:13:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/b9/19/1ce6bf444f858b83a733171306134a0544eaddf1ca8851ede6540a55b2ad/coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c79cae102bb3b1801e2ef1511fb50e91ec83a1ce466b2c7c25010d884336de46", size = 247825, upload-time = "2025-10-15T15:13:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/71/0b/d3bcbbc259fcced5fb67c5d78f6e7ee965f49760c14afd931e9e663a83b2/coverage-7.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16ce17ceb5d211f320b62df002fa7016b7442ea0fd260c11cec8ce7730954893", size = 250573, upload-time = "2025-10-15T15:13:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/58/8d/b0ff3641a320abb047258d36ed1c21d16be33beed4152628331a1baf3365/coverage-7.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80027673e9d0bd6aef86134b0771845e2da85755cf686e7c7c59566cf5a89115", size = 251706, upload-time = "2025-10-15T15:13:09.4Z" }, + { url = "https://files.pythonhosted.org/packages/59/c8/5a586fe8c7b0458053d9c687f5cff515a74b66c85931f7fe17a1c958b4ac/coverage-7.11.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d3ffa07a08657306cd2215b0da53761c4d73cb54d9143b9303a6481ec0cd415", size = 248221, upload-time = "2025-10-15T15:13:10.964Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ff/3a25e3132804ba44cfa9a778cdf2b73dbbe63ef4b0945e39602fc896ba52/coverage-7.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a3b6a5f8b2524fd6c1066bc85bfd97e78709bb5e37b5b94911a6506b65f47186", size = 249624, upload-time = "2025-10-15T15:13:12.5Z" }, + { url = "https://files.pythonhosted.org/packages/c5/12/ff10c8ce3895e1b17a73485ea79ebc1896a9e466a9d0f4aef63e0d17b718/coverage-7.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fcc0a4aa589de34bc56e1a80a740ee0f8c47611bdfb28cd1849de60660f3799d", size = 247744, upload-time = "2025-10-15T15:13:14.554Z" }, + { url = "https://files.pythonhosted.org/packages/16/02/d500b91f5471b2975947e0629b8980e5e90786fe316b6d7299852c1d793d/coverage-7.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dba82204769d78c3fd31b35c3d5f46e06511936c5019c39f98320e05b08f794d", size = 247325, upload-time = "2025-10-15T15:13:16.438Z" }, + { url = "https://files.pythonhosted.org/packages/77/11/dee0284fbbd9cd64cfce806b827452c6df3f100d9e66188e82dfe771d4af/coverage-7.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81b335f03ba67309a95210caf3eb43bd6fe75a4e22ba653ef97b4696c56c7ec2", size = 249180, upload-time = "2025-10-15T15:13:17.959Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/cdf1def928f0a150a057cab03286774e73e29c2395f0d30ce3d9e9f8e697/coverage-7.11.0-cp312-cp312-win32.whl", hash = "sha256:037b2d064c2f8cc8716fe4d39cb705779af3fbf1ba318dc96a1af858888c7bb5", size = 218479, upload-time = "2025-10-15T15:13:19.608Z" }, + { url = "https://files.pythonhosted.org/packages/ff/55/e5884d55e031da9c15b94b90a23beccc9d6beee65e9835cd6da0a79e4f3a/coverage-7.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:d66c0104aec3b75e5fd897e7940188ea1892ca1d0235316bf89286d6a22568c0", size = 219290, upload-time = "2025-10-15T15:13:21.593Z" }, + { url = "https://files.pythonhosted.org/packages/23/a8/faa930cfc71c1d16bc78f9a19bb73700464f9c331d9e547bfbc1dbd3a108/coverage-7.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:d91ebeac603812a09cf6a886ba6e464f3bbb367411904ae3790dfe28311b15ad", size = 217924, upload-time = "2025-10-15T15:13:23.39Z" }, + { url = "https://files.pythonhosted.org/packages/60/7f/85e4dfe65e400645464b25c036a26ac226cf3a69d4a50c3934c532491cdd/coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1", size = 216129, upload-time = "2025-10-15T15:13:25.371Z" }, + { url = "https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be", size = 216380, upload-time = "2025-10-15T15:13:26.976Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f5/3da9cc9596708273385189289c0e4d8197d37a386bdf17619013554b3447/coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d", size = 247375, upload-time = "2025-10-15T15:13:28.923Z" }, + { url = "https://files.pythonhosted.org/packages/65/6c/f7f59c342359a235559d2bc76b0c73cfc4bac7d61bb0df210965cb1ecffd/coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82", size = 249978, upload-time = "2025-10-15T15:13:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/e7/8c/042dede2e23525e863bf1ccd2b92689692a148d8b5fd37c37899ba882645/coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52", size = 251253, upload-time = "2025-10-15T15:13:32.174Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/3c58df67bfa809a7bddd786356d9c5283e45d693edb5f3f55d0986dd905a/coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b", size = 247591, upload-time = "2025-10-15T15:13:34.147Z" }, + { url = "https://files.pythonhosted.org/packages/26/5b/c7f32efd862ee0477a18c41e4761305de6ddd2d49cdeda0c1116227570fd/coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4", size = 249411, upload-time = "2025-10-15T15:13:38.425Z" }, + { url = "https://files.pythonhosted.org/packages/76/b5/78cb4f1e86c1611431c990423ec0768122905b03837e1b4c6a6f388a858b/coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd", size = 247303, upload-time = "2025-10-15T15:13:40.464Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/23c753a8641a330f45f221286e707c427e46d0ffd1719b080cedc984ec40/coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc", size = 247157, upload-time = "2025-10-15T15:13:42.087Z" }, + { url = "https://files.pythonhosted.org/packages/c5/42/6e0cc71dc8a464486e944a4fa0d85bdec031cc2969e98ed41532a98336b9/coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48", size = 248921, upload-time = "2025-10-15T15:13:43.715Z" }, + { url = "https://files.pythonhosted.org/packages/e8/1c/743c2ef665e6858cccb0f84377dfe3a4c25add51e8c7ef19249be92465b6/coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040", size = 218526, upload-time = "2025-10-15T15:13:45.336Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d5/226daadfd1bf8ddbccefbd3aa3547d7b960fb48e1bdac124e2dd13a2b71a/coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05", size = 219317, upload-time = "2025-10-15T15:13:47.401Z" }, + { url = "https://files.pythonhosted.org/packages/97/54/47db81dcbe571a48a298f206183ba8a7ba79200a37cd0d9f4788fcd2af4a/coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a", size = 217948, upload-time = "2025-10-15T15:13:49.096Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8b/cb68425420154e7e2a82fd779a8cc01549b6fa83c2ad3679cd6c088ebd07/coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b", size = 216837, upload-time = "2025-10-15T15:13:51.09Z" }, + { url = "https://files.pythonhosted.org/packages/33/55/9d61b5765a025685e14659c8d07037247de6383c0385757544ffe4606475/coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37", size = 217061, upload-time = "2025-10-15T15:13:52.747Z" }, + { url = "https://files.pythonhosted.org/packages/52/85/292459c9186d70dcec6538f06ea251bc968046922497377bf4a1dc9a71de/coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de", size = 258398, upload-time = "2025-10-15T15:13:54.45Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e2/46edd73fb8bf51446c41148d81944c54ed224854812b6ca549be25113ee0/coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f", size = 260574, upload-time = "2025-10-15T15:13:56.145Z" }, + { url = "https://files.pythonhosted.org/packages/07/5e/1df469a19007ff82e2ca8fe509822820a31e251f80ee7344c34f6cd2ec43/coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c", size = 262797, upload-time = "2025-10-15T15:13:58.635Z" }, + { url = "https://files.pythonhosted.org/packages/f9/50/de216b31a1434b94d9b34a964c09943c6be45069ec704bfc379d8d89a649/coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa", size = 257361, upload-time = "2025-10-15T15:14:00.409Z" }, + { url = "https://files.pythonhosted.org/packages/82/1e/3f9f8344a48111e152e0fd495b6fff13cc743e771a6050abf1627a7ba918/coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740", size = 260349, upload-time = "2025-10-15T15:14:02.188Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/3f52741f9e7d82124272f3070bbe316006a7de1bad1093f88d59bfc6c548/coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef", size = 258114, upload-time = "2025-10-15T15:14:03.907Z" }, + { url = "https://files.pythonhosted.org/packages/0b/8b/918f0e15f0365d50d3986bbd3338ca01178717ac5678301f3f547b6619e6/coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0", size = 256723, upload-time = "2025-10-15T15:14:06.324Z" }, + { url = "https://files.pythonhosted.org/packages/44/9e/7776829f82d3cf630878a7965a7d70cc6ca94f22c7d20ec4944f7148cb46/coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca", size = 259238, upload-time = "2025-10-15T15:14:08.002Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b8/49cf253e1e7a3bedb85199b201862dd7ca4859f75b6cf25ffa7298aa0760/coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2", size = 219180, upload-time = "2025-10-15T15:14:09.786Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e1/1a541703826be7ae2125a0fb7f821af5729d56bb71e946e7b933cc7a89a4/coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268", size = 220241, upload-time = "2025-10-15T15:14:11.471Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/5ee0e0a08621140fd418ec4020f595b4d52d7eb429ae6a0c6542b4ba6f14/coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836", size = 218510, upload-time = "2025-10-15T15:14:13.46Z" }, + { url = "https://files.pythonhosted.org/packages/f4/06/e923830c1985ce808e40a3fa3eb46c13350b3224b7da59757d37b6ce12b8/coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497", size = 216110, upload-time = "2025-10-15T15:14:15.157Z" }, + { url = "https://files.pythonhosted.org/packages/42/82/cdeed03bfead45203fb651ed756dfb5266028f5f939e7f06efac4041dad5/coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e", size = 216395, upload-time = "2025-10-15T15:14:16.863Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ba/e1c80caffc3199aa699813f73ff097bc2df7b31642bdbc7493600a8f1de5/coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1", size = 247433, upload-time = "2025-10-15T15:14:18.589Z" }, + { url = "https://files.pythonhosted.org/packages/80/c0/5b259b029694ce0a5bbc1548834c7ba3db41d3efd3474489d7efce4ceb18/coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca", size = 249970, upload-time = "2025-10-15T15:14:20.307Z" }, + { url = "https://files.pythonhosted.org/packages/8c/86/171b2b5e1aac7e2fd9b43f7158b987dbeb95f06d1fbecad54ad8163ae3e8/coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd", size = 251324, upload-time = "2025-10-15T15:14:22.419Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/7e10414d343385b92024af3932a27a1caf75c6e27ee88ba211221ff1a145/coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43", size = 247445, upload-time = "2025-10-15T15:14:24.205Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3b/e4f966b21f5be8c4bf86ad75ae94efa0de4c99c7bbb8114476323102e345/coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777", size = 249324, upload-time = "2025-10-15T15:14:26.234Z" }, + { url = "https://files.pythonhosted.org/packages/00/a2/8479325576dfcd909244d0df215f077f47437ab852ab778cfa2f8bf4d954/coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2", size = 247261, upload-time = "2025-10-15T15:14:28.42Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d8/3a9e2db19d94d65771d0f2e21a9ea587d11b831332a73622f901157cc24b/coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d", size = 247092, upload-time = "2025-10-15T15:14:30.784Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b1/bbca3c472544f9e2ad2d5116b2379732957048be4b93a9c543fcd0207e5f/coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4", size = 248755, upload-time = "2025-10-15T15:14:32.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/49/638d5a45a6a0f00af53d6b637c87007eb2297042186334e9923a61aa8854/coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721", size = 218793, upload-time = "2025-10-15T15:14:34.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/cc/b675a51f2d068adb3cdf3799212c662239b0ca27f4691d1fff81b92ea850/coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad", size = 219587, upload-time = "2025-10-15T15:14:37.047Z" }, + { url = "https://files.pythonhosted.org/packages/93/98/5ac886876026de04f00820e5094fe22166b98dcb8b426bf6827aaf67048c/coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479", size = 218168, upload-time = "2025-10-15T15:14:38.861Z" }, + { url = "https://files.pythonhosted.org/packages/14/d1/b4145d35b3e3ecf4d917e97fc8895bcf027d854879ba401d9ff0f533f997/coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f", size = 216850, upload-time = "2025-10-15T15:14:40.651Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d1/7f645fc2eccd318369a8a9948acc447bb7c1ade2911e31d3c5620544c22b/coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e", size = 217071, upload-time = "2025-10-15T15:14:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/64d124649db2737ceced1dfcbdcb79898d5868d311730f622f8ecae84250/coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44", size = 258570, upload-time = "2025-10-15T15:14:44.542Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3f/6f5922f80dc6f2d8b2c6f974835c43f53eb4257a7797727e6ca5b7b2ec1f/coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3", size = 260738, upload-time = "2025-10-15T15:14:46.436Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5f/9e883523c4647c860b3812b417a2017e361eca5b635ee658387dc11b13c1/coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b", size = 262994, upload-time = "2025-10-15T15:14:48.3Z" }, + { url = "https://files.pythonhosted.org/packages/07/bb/43b5a8e94c09c8bf51743ffc65c4c841a4ca5d3ed191d0a6919c379a1b83/coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d", size = 257282, upload-time = "2025-10-15T15:14:50.236Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e5/0ead8af411411330b928733e1d201384b39251a5f043c1612970310e8283/coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2", size = 260430, upload-time = "2025-10-15T15:14:52.413Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/03dd8bb0ba5b971620dcaac145461950f6d8204953e535d2b20c6b65d729/coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e", size = 258190, upload-time = "2025-10-15T15:14:54.268Z" }, + { url = "https://files.pythonhosted.org/packages/45/ae/28a9cce40bf3174426cb2f7e71ee172d98e7f6446dff936a7ccecee34b14/coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996", size = 256658, upload-time = "2025-10-15T15:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/5c/7c/3a44234a8599513684bfc8684878fd7b126c2760f79712bb78c56f19efc4/coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11", size = 259342, upload-time = "2025-10-15T15:14:58.538Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e6/0108519cba871af0351725ebdb8660fd7a0fe2ba3850d56d32490c7d9b4b/coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73", size = 219568, upload-time = "2025-10-15T15:15:00.382Z" }, + { url = "https://files.pythonhosted.org/packages/c9/76/44ba876e0942b4e62fdde23ccb029ddb16d19ba1bef081edd00857ba0b16/coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547", size = 220687, upload-time = "2025-10-15T15:15:02.322Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0c/0df55ecb20d0d0ed5c322e10a441775e1a3a5d78c60f0c4e1abfe6fcf949/coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3", size = 218711, upload-time = "2025-10-15T15:15:04.575Z" }, + { url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + [[package]] name = "et-xmlfile" version = "2.0.0" @@ -149,6 +315,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] +[[package]] +name = "execnet" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/ff/b4c0dc78fbe20c3e59c0c7334de0c27eb4001a2b2017999af398bf730817/execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3", size = 166524, upload-time = "2024-04-08T09:04:19.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612, upload-time = "2024-04-08T09:04:17.414Z" }, +] + [[package]] name = "filelock" version = "3.20.0" @@ -158,6 +333,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "griffe" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/d7/6c09dd7ce4c7837e4cdb11dce980cb45ae3cd87677298dc3b781b6bce7d3/griffe-1.14.0.tar.gz", hash = "sha256:9d2a15c1eca966d68e00517de5d69dd1bc5c9f2335ef6c1775362ba5b8651a13", size = 424684, upload-time = "2025-09-05T15:02:29.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/b1/9ff6578d789a89812ff21e4e0f80ffae20a65d5dd84e7a17873fe3b365be/griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0", size = 144439, upload-time = "2025-09-05T15:02:27.511Z" }, +] + +[[package]] +name = "identify" +version = "2.6.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -167,6 +375,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + [[package]] name = "joblib" version = "1.5.2" @@ -176,6 +405,238 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241", size = 308396, upload-time = "2025-08-27T12:15:45.188Z" }, ] +[[package]] +name = "markdown" +version = "3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/37/02347f6d6d8279247a5837082ebc26fc0d5aaeaf75aa013fcbb433c777ab/markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a", size = 364585, upload-time = "2025-09-04T20:25:22.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/ae/44c4a6a4cbb496d93c6257954260fe3a6e91b7bed2240e5dad2a717f5111/markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280", size = 107441, upload-time = "2025-09-04T20:25:21.784Z" }, +] + +[[package]] +name = "markupsafe" +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" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/fa/9124cd63d822e2bcbea1450ae68cdc3faf3655c69b455f3a7ed36ce6c628/mkdocs_autorefs-1.4.3.tar.gz", hash = "sha256:beee715b254455c4aa93b6ef3c67579c399ca092259cc41b7d9342573ff1fc75", size = 55425, upload-time = "2025-08-26T14:23:17.223Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/4d/7123b6fa2278000688ebd338e2a06d16870aaf9eceae6ba047ea05f92df1/mkdocs_autorefs-1.4.3-py3-none-any.whl", hash = "sha256:469d85eb3114801d08e9cc55d102b3ba65917a869b893403b8987b601cf55dc9", size = 25034, upload-time = "2025-08-26T14:23:15.906Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.6.22" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/5d/317e37b6c43325cb376a1d6439df9cc743b8ee41c84603c2faf7286afc82/mkdocs_material-9.6.22.tar.gz", hash = "sha256:87c158b0642e1ada6da0cbd798a3389b0bc5516b90e5ece4a0fb939f00bacd1c", size = 4044968, upload-time = "2025-10-15T09:21:15.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/82/6fdb9a7a04fb222f4849ffec1006f891a0280825a20314d11f3ccdee14eb/mkdocs_material-9.6.22-py3-none-any.whl", hash = "sha256:14ac5f72d38898b2f98ac75a5531aaca9366eaa427b0f49fc2ecf04d99b7ad84", size = 9206252, upload-time = "2025-10-15T09:21:12.175Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "0.30.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/33/2fa3243439f794e685d3e694590d28469a9b8ea733af4b48c250a3ffc9a0/mkdocstrings-0.30.1.tar.gz", hash = "sha256:84a007aae9b707fb0aebfc9da23db4b26fc9ab562eb56e335e9ec480cb19744f", size = 106350, upload-time = "2025-09-19T10:49:26.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/2c/f0dc4e1ee7f618f5bff7e05898d20bf8b6e7fa612038f768bfa295f136a4/mkdocstrings-0.30.1-py3-none-any.whl", hash = "sha256:41bd71f284ca4d44a668816193e4025c950b002252081e387433656ae9a70a82", size = 36704, upload-time = "2025-09-19T10:49:24.805Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "1.18.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffe" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/ae/58ab2bfbee2792e92a98b97e872f7c003deb903071f75d8d83aa55db28fa/mkdocstrings_python-1.18.2.tar.gz", hash = "sha256:4ad536920a07b6336f50d4c6d5603316fafb1172c5c882370cbbc954770ad323", size = 207972, upload-time = "2025-08-28T16:11:19.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/8f/ce008599d9adebf33ed144e7736914385e8537f5fc686fdb7cceb8c22431/mkdocstrings_python-1.18.2-py3-none-any.whl", hash = "sha256:944fe6deb8f08f33fa936d538233c4036e9f53e840994f6146e8e94eb71b600d", size = 138215, upload-time = "2025-08-28T16:11:18.176Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +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" @@ -328,7 +789,7 @@ wheels = [ [[package]] name = "oda-reader" -version = "1.3.0" +version = "1.3.0.post0" source = { editable = "." } dependencies = [ { name = "filelock" }, @@ -343,8 +804,20 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "pre-commit" }, { name = "ruff" }, ] +docs = [ + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extra = ["python"] }, +] +test = [ + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pytest-xdist" }, +] [package.metadata] requires-dist = [ @@ -359,7 +832,21 @@ requires-dist = [ ] [package.metadata.requires-dev] -dev = [{ name = "ruff", specifier = ">=0.14.0" }] +dev = [ + { name = "pre-commit", specifier = ">=4.0.0" }, + { name = "ruff", specifier = ">=0.14.0" }, +] +docs = [ + { name = "mkdocs", specifier = ">=1.5.0" }, + { name = "mkdocs-material", specifier = ">=9.5.0" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=0.24.0" }, +] +test = [ + { name = "pytest", specifier = ">=8.0" }, + { name = "pytest-cov", specifier = ">=4.1" }, + { name = "pytest-mock", specifier = ">=3.12" }, + { name = "pytest-xdist", specifier = ">=3.5" }, +] [[package]] name = "openpyxl" @@ -373,6 +860,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, ] +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + [[package]] name = "pandas" version = "2.3.3" @@ -435,6 +940,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, ] +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + [[package]] name = "platformdirs" version = "4.5.0" @@ -444,6 +958,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, +] + [[package]] name = "pyarrow" version = "21.0.0" @@ -487,6 +1026,85 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/4e/519c1bc1876625fe6b71e9a28287c43ec2f20f73c658b9ae1d485c0c206e/pyarrow-21.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:222c39e2c70113543982c6b34f3077962b44fca38c0bd9e68bb6781534425c10", size = 26371006, upload-time = "2025-07-18T00:56:56.379Z" }, ] +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/b3/6d2b3f149bc5413b0a29761c2c5832d8ce904a1d7f621e86616d96f505cc/pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91", size = 853277, upload-time = "2025-07-28T16:19:34.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178, upload-time = "2025-07-28T16:19:31.401Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +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/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -508,6 +1126,82 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + [[package]] name = "requests" version = "2.32.5" @@ -575,6 +1269,55 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -613,3 +1356,50 @@ sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599 wheels = [ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] + +[[package]] +name = "virtualenv" +version = "20.35.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/d5/b0ccd381d55c8f45d46f77df6ae59fbc23d19e901e2d523395598e5f4c93/virtualenv-20.35.3.tar.gz", hash = "sha256:4f1a845d131133bdff10590489610c98c168ff99dc75d6c96853801f7f67af44", size = 6002907, upload-time = "2025-10-10T21:23:33.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/73/d9a94da0e9d470a543c1b9d3ccbceb0f59455983088e727b8a1824ed90fb/virtualenv-20.35.3-py3-none-any.whl", hash = "sha256:63d106565078d8c8d0b206d48080f938a8b25361e19432d2c9db40d2899c810a", size = 5981061, upload-time = "2025-10-10T21:23:30.433Z" }, +] + +[[package]] +name = "watchdog" +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" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +]