From f8f6845dd771bbeda4a3c4cb158c831088cc9c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Kaczmarek?= <12432719+AN0DA@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:52:34 +0200 Subject: [PATCH] chore: migrate documentation from Sphinx to MkDocs - Updated the documentation build process to use MkDocs instead of Sphinx, enhancing the user experience with a more modern interface. - Added a new MkDocs configuration file and adjusted the Makefile for building the documentation. - Removed Sphinx-related files and configurations, including the previous GitHub Actions workflow for Sphinx. - Introduced new documentation files and updated existing ones to align with the MkDocs structure. - Updated the README to reflect the new documentation build instructions and structure. --- .github/workflows/mkdocs.yml | 115 ++++++ .github/workflows/sphinx.yml | 80 ---- .gitignore | 2 +- Makefile | 2 +- README.md | 21 +- docs/Makefile | 20 - docs/_static/.gitkeep | 2 +- docs/access_layer.md | 584 +++++++++++++++++++++++++++ docs/access_layer.rst | 676 ------------------------------- docs/api_clients.md | 139 +++++++ docs/api_clients.rst | 221 ---------- docs/appendix.md | 301 ++++++++++++++ docs/appendix.rst | 287 ------------- docs/changelog.md | 3 +- docs/conf.py | 41 -- docs/config.md | 458 +++++++++++++++++++++ docs/config.rst | 477 ---------------------- docs/index.md | 90 +++++ docs/index.rst | 95 ----- docs/main_client.md | 193 +++++++++ docs/main_client.rst | 191 --------- docs/make.bat | 35 -- docs/rate_limiting.md | 381 +++++++++++++++++ docs/rate_limiting.rst | 365 ----------------- mkdocs.yml | 93 +++++ pybdl/utils/__init__.py | 1 + pyproject.toml | 9 +- uv.lock | 763 +++++++++++++++-------------------- 28 files changed, 2710 insertions(+), 2935 deletions(-) create mode 100644 .github/workflows/mkdocs.yml delete mode 100644 .github/workflows/sphinx.yml delete mode 100644 docs/Makefile create mode 100644 docs/access_layer.md delete mode 100644 docs/access_layer.rst create mode 100644 docs/api_clients.md delete mode 100644 docs/api_clients.rst create mode 100644 docs/appendix.md delete mode 100644 docs/appendix.rst delete mode 100644 docs/conf.py create mode 100644 docs/config.md delete mode 100644 docs/config.rst create mode 100644 docs/index.md delete mode 100644 docs/index.rst create mode 100644 docs/main_client.md delete mode 100644 docs/main_client.rst delete mode 100644 docs/make.bat create mode 100644 docs/rate_limiting.md delete mode 100644 docs/rate_limiting.rst create mode 100644 mkdocs.yml create mode 100644 pybdl/utils/__init__.py diff --git a/.github/workflows/mkdocs.yml b/.github/workflows/mkdocs.yml new file mode 100644 index 0000000..37664eb --- /dev/null +++ b/.github/workflows/mkdocs.yml @@ -0,0 +1,115 @@ +--- +name: Docs + +"on": + push: + branches: [main] + paths: + - "docs/**" + - "pybdl/**" + - ".github/workflows/mkdocs.yml" + - "mkdocs.yml" + - "pyproject.toml" + - "CHANGELOG.md" + pull_request: + paths: + - "docs/**" + - "pybdl/**" + - ".github/workflows/mkdocs.yml" + - "mkdocs.yml" + - "pyproject.toml" + +concurrency: + group: docs-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +jobs: + build: + name: Build MkDocs site + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@94527f2e458b27549849d47d273a16bec83a01e9 # v7 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + + - name: Set up Python + run: uv python install + + - name: Install project and docs dependencies + run: uv sync --group docs + + - name: Build site (strict) + run: uv run mkdocs build --strict + + - name: Upload site artifact + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 + with: + name: mkdocs-site + path: site + retention-days: 7 + + deploy: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + name: Deploy versioned docs (mike → gh-pages) + needs: build + runs-on: ubuntu-latest + permissions: + contents: write # Push versioned HTML and mike metadata to the gh-pages branch. + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Authenticate git for push + run: git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: Install uv + uses: astral-sh/setup-uv@94527f2e458b27549849d47d273a16bec83a01e9 # v7 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + + - name: Set up Python + run: uv python install + + - name: Install project and docs dependencies + run: uv sync --group docs + + - name: Configure Git for mike + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Read version from pyproject.toml + id: version + run: | + v="$(uv run python -c \ + 'import tomllib as t;print(t.load(open("pyproject.toml","rb"))["project"]["version"])')" + echo "version=${v}" >> "${GITHUB_OUTPUT}" + + - name: Deploy to gh-pages with mike + env: + MIKE_VERSION: ${{ steps.version.outputs.version }} + run: > + uv run mike deploy + --push + --update-aliases + "${MIKE_VERSION}" + latest + + - name: Point site root at latest (redirect) + run: uv run mike set-default latest --push diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml deleted file mode 100644 index be8113a..0000000 --- a/.github/workflows/sphinx.yml +++ /dev/null @@ -1,80 +0,0 @@ ---- -name: Docs - -"on": - push: - branches: [main] - paths: - - "docs/**" - - "pybdl/**" - - ".github/workflows/sphinx.yml" - - "pyproject.toml" - pull_request: - paths: - - "docs/**" - - "pybdl/**" - - ".github/workflows/sphinx.yml" - - "pyproject.toml" - -concurrency: - group: docs-${{ github.ref }} - cancel-in-progress: true - -permissions: {} - -jobs: - build: - name: Build Sphinx docs - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Install uv - uses: astral-sh/setup-uv@94527f2e458b27549849d47d273a16bec83a01e9 # v7 - with: - enable-cache: true - cache-dependency-glob: "uv.lock" - - - name: Set up Python - run: uv python install - - - name: Install docs dependencies - run: uv sync --only-group docs - - - name: Build HTML (fail on warnings) - run: > - uv run sphinx-build -W --keep-going -b html docs docs/_build/html - - - name: Upload docs as artifact (PRs & pushes) - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 - with: - name: html-docs - path: docs/_build/html - retention-days: 7 - - - name: Upload GitHub Pages artifact (only main) - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4 - with: - path: docs/_build/html - - deploy: - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - name: Deploy to GitHub Pages - needs: build - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - permissions: - pages: write # deploy-pages: publish to GitHub Pages - id-token: write # deploy-pages: OIDC authentication with Pages - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 diff --git a/.gitignore b/.gitignore index 551a69d..7fe3557 100644 --- a/.gitignore +++ b/.gitignore @@ -97,7 +97,7 @@ instance/ # Scrapy stuff: .scrapy -# Sphinx documentation +# Legacy Sphinx output (MkDocs uses /site at repo root) docs/_build/ # PyBuilder diff --git a/Makefile b/Makefile index 8d1a678..3a38cc0 100644 --- a/Makefile +++ b/Makefile @@ -14,4 +14,4 @@ test: uv run pytest --cov --cov-report term-missing:skip-covered docs: - uv run sphinx-build -W --keep-going -b html docs docs/_build/html + uv run --group docs mkdocs build --strict diff --git a/README.md b/README.md index 9c4b1a0..4fcf743 100644 --- a/README.md +++ b/README.md @@ -103,21 +103,26 @@ payload_with_meta = bdl.api.data.get_data_by_variable_with_metadata( ## Documentation -Full documentation is built with Sphinx. To build locally: +Documentation uses [MkDocs Material](https://squidfunk.github.io/mkdocs-material/) with +[mkdocstrings](https://mkdocstrings.github.io/) and [mike](https://github.com/jimporter/mike) for versioned +[GitHub Pages](https://pages.github.com/) (`gh-pages` branch). To build locally: ```bash +uv sync --group docs make docs -# or: cd docs && make html +# or: uv run mkdocs serve # live reload at http://127.0.0.1:8000 ``` -The built HTML is written to `docs/_build/html/`. +The static site is written to `site/` (gitignored). Published URLs use version directories and a `latest` +alias; configure the repository **Settings → Pages** to deploy from the **`gh-pages`** branch at **`/` +(root)**. Key documentation pages: -- [Main Client](docs/main_client.rst) — BDL client, access vs API layer, context manager, async -- [Access Layer](docs/access_layer.rst) — DataFrame interface, enrichment, pagination, metadata -- [API Clients](docs/api_clients.rst) — Low-level raw-JSON clients -- [Configuration](docs/config.rst) — All options and environment variables -- [Rate Limiting](docs/rate_limiting.rst) — Quotas, retry, custom limits +- [Main client](docs/main_client.md) — BDL client, access vs API layer, context manager, async +- [Access layer](docs/access_layer.md) — DataFrame interface, enrichment, pagination, metadata +- [API clients](docs/api_clients.md) — Low-level raw-JSON clients +- [Configuration](docs/config.md) — All options and environment variables +- [Rate limiting](docs/rate_limiting.md) — Quotas, retry, custom limits - [Examples](docs/examples.ipynb) — Jupyter notebook with real-world examples - [Changelog](CHANGELOG.md) — Release history diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index d4bb2cb..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/.gitkeep b/docs/_static/.gitkeep index 5371455..a25dcaa 100644 --- a/docs/_static/.gitkeep +++ b/docs/_static/.gitkeep @@ -1,2 +1,2 @@ # This file ensures the _static directory is tracked in git -# Sphinx will use this directory for custom static files if needed +# Optional static assets for MkDocs (e.g. extra_css / extra_javascript) diff --git a/docs/access_layer.md b/docs/access_layer.md new file mode 100644 index 0000000..2795f5a --- /dev/null +++ b/docs/access_layer.md @@ -0,0 +1,584 @@ +# Access Layer + +The access layer is the **primary user-facing interface** of pyBDL. It +provides a clean, pandas DataFrame-based API that automatically handles +data conversion and normalization. + +## Overview + +The access layer sits on top of the raw API clients and provides: + +- **Automatic DataFrame conversion**: All responses are converted to + pandas DataFrames +- **Column name normalization**: camelCase API fields are converted to + snake_case +- **Data type inference**: Proper types (integers, floats, booleans) are + automatically detected +- **Nested data normalization**: Complex nested structures are flattened + into tabular format + +The main client provides two interfaces: + +- **Access layer** (default): Returns pandas DataFrames - use + `bdl.levels`, + `bdl.data`, etc. +- **API layer**: Returns raw dictionaries - use + `bdl.api.levels`, + `bdl.api.data`, etc. + +For most users, the access layer is recommended as it provides a more +Pythonic and data-analysis-friendly interface. + +## Quick Start + +``` python +from pybdl import BDL, BDLConfig + +# Initialize client +bdl = BDL(BDLConfig(api_key="your-api-key")) + +# Access layer returns DataFrames +levels_df = bdl.levels.list_levels() +print(levels_df.head()) + +# Data is ready for analysis +print(levels_df.dtypes) +print(levels_df.columns) +``` + +## Key Features + +### DataFrame Conversion + +All access layer methods return pandas DataFrames, making data +immediately ready for analysis: + +``` python +# Get variables as DataFrame +variables_df = bdl.variables.list_variables() + +# Use pandas operations directly +filtered = variables_df[variables_df['name'].str.contains('population', case=False)] +sorted_vars = variables_df.sort_values('name') +``` + +### Column Name Normalization + +API responses use camelCase (e.g., `variableId`, `unitName`), but the +access layer converts these to snake_case (e.g., `variable_id`, +`unit_name`) for Pythonic access: + +``` python +df = bdl.variables.get_variable("3643") +# Columns are: variable_id, name, description (not variableId, Name, Description) +print(df.columns) +``` + +### Data Type Inference + +The access layer automatically infers and converts data types: + +``` python +df = bdl.data.get_data_by_variable("3643", years=[2021]) +# year column is Int64, val column is float +print(df.dtypes) +``` + +### Nested Data Normalization + +The data endpoints return nested structures. The access layer +automatically flattens them: + +```python +# API returns: [{"id": "1", "name": "Warsaw", +# "values": [{"year": 2021, "val": 1000}, ...]}] +# Access layer returns flat DataFrame: +df = bdl.data.get_data_by_variable("3643", years=[2021]) +# Columns: unit_id, unit_name, year, val, attr_id +print(df.head()) +``` + +## Available Endpoints + +The access layer provides endpoints for all BDL API resources: + +| Endpoint | Access Method | Description | +|------------|------------------|----------------------------| +| Aggregates | `bdl.aggregates` | Aggregation level metadata | +| Attributes | `bdl.attributes` | Attribute metadata | +| Data | `bdl.data` | Statistical data access | +| Levels | `bdl.levels` | Administrative unit levels | +| Measures | `bdl.measures` | Measure unit metadata | +| Subjects | `bdl.subjects` | Subject hierarchy | +| Units | `bdl.units` | Administrative units | +| Variables | `bdl.variables` | Variable metadata | +| Years | `bdl.years` | Available years | + +Available Access Endpoints + +## Endpoint Details + +### Levels + +Administrative unit aggregation levels (country, voivodeship, county, +municipality): + +``` python +# List all levels +levels_df = bdl.levels.list_levels() + +# Get specific level +level_df = bdl.levels.get_level(1) # Level 1 = country + +# Get metadata +metadata_df = bdl.levels.get_levels_metadata() +``` + +### Subjects + +Subject categories and hierarchy: + +``` python +# List all top-level subjects +subjects_df = bdl.subjects.list_subjects() + +# Get subjects under a parent +child_subjects = bdl.subjects.list_subjects(parent_id="P0001") + +# Search subjects +results = bdl.subjects.search_subjects(name="population") + +# Get specific subject +subject_df = bdl.subjects.get_subject("P0001") +``` + +### Variables + +Statistical variables (indicators): + +``` python +# List all variables +variables_df = bdl.variables.list_variables() + +# Filter variables +filtered = bdl.variables.list_variables( + category_id="P0001", + name="population" +) + +# Search variables +results = bdl.variables.search_variables(name="unemployment") + +# Get specific variable +variable_df = bdl.variables.get_variable("3643") +``` + +### Data + +Statistical data retrieval: + +``` python +# Get data by variable (most common) +df = bdl.data.get_data_by_variable( + variable_id="3643", + years=[2021], + unit_level=2 # Voivodeship level +) + +# Get data for multiple years +df = bdl.data.get_data_by_variable( + variable_id="3643", + years=[2020, 2021, 2022], + unit_level=2 +) + +# Get data with aggregate filter +df = bdl.data.get_data_by_variable( + variable_id="3643", + years=[2021], + aggregate_id=1 +) + +# Get data by administrative unit +df = bdl.data.get_data_by_unit( + unit_id="020000000000", + variable_ids=["3643"], + years=[2021] +) + +# Get data for a locality +df = bdl.data.get_data_by_variable_locality( + variable_id="3643", + unit_parent_id="1465011", + years=[2021] +) + +# Get data by unit locality +df = bdl.data.get_data_by_unit_locality( + unit_id="1465011", + variable_id="3643", + years=[2021] +) +``` + +The data endpoints automatically normalize nested `values` arrays into +flat rows. + +#### Accessing Pagination Metadata + +Use `return_metadata=True` to receive a `(DataFrame, metadata)` tuple +alongside the data. The metadata dictionary contains information from +the first response page, including pagination details such as +`totalPages` and `totalRecords`. + +``` python +# Returns (DataFrame, metadata_dict) +df, meta = bdl.data.get_data_by_variable( + variable_id="3643", + years=[2021], + return_metadata=True, +) +print(meta.get("totalPages")) +print(meta.get("totalRecords")) +``` + +Convenience `*_with_metadata` wrappers always return a tuple: + +``` python +df, meta = bdl.data.get_data_by_variable_with_metadata(variable_id="3643", years=[2021]) +df, meta = bdl.data.get_data_by_unit_with_metadata(unit_id="020000000000", variable_ids=["3643"]) +df, meta = bdl.data.get_data_by_variable_locality_with_metadata( + variable_id="3643", unit_parent_id="1465011", years=[2021] +) +df, meta = bdl.data.get_data_by_unit_locality_with_metadata( + unit_id="1465011", variable_ids=["3643"] +) +``` + +### Units + +Administrative units (regions, cities, etc.): + +``` python +# List units by level +voivodeships = bdl.units.list_units(level=2) # Level 2 = voivodeship + +# Search units +warsaw = bdl.units.search_units(name="Warsaw") + +# Get specific unit +unit_df = bdl.units.get_unit("020000000000") + +# List localities (statistical localities) +localities = bdl.units.list_localities(level=6) # Level 6 = municipality + +# Search localities +warsaw_localities = bdl.units.search_localities(name="Warsaw", level=6) + +# Get specific locality +locality_df = bdl.units.get_locality("1465011") +``` + +### Attributes + +Data attributes (dimensions): + +``` python +# List all attributes +attributes_df = bdl.attributes.list_attributes() + +# Get specific attribute +attr_df = bdl.attributes.get_attribute("1") +``` + +### Measures + +Measure units: + +``` python +# List all measures +measures_df = bdl.measures.list_measures() + +# Get specific measure +measure_df = bdl.measures.get_measure(1) +``` + +### Aggregates + +Aggregation types: + +``` python +# List all aggregates +aggregates_df = bdl.aggregates.list_aggregates() + +# Get specific aggregate +aggregate_df = bdl.aggregates.get_aggregate("1") +``` + +### Years + +Available years for data: + +``` python +# List all available years +years_df = bdl.years.list_years() + +# Get specific year metadata +year_df = bdl.years.get_year(2021) +``` + +## Enrichment + +Many access layer methods accept an `enrich` parameter (or individual +`enrich_*` flags) to automatically join human-readable reference data +into the returned DataFrame. Enrichment fetches the required lookup +table once per client session, caches it in memory, and left-joins the +resolved columns onto each result row. + +### Usage + +Pass a list of dimension names to `enrich`: + +``` python +# Enrich variables with level names, measure descriptions, and subject names +variables = bdl.variables.list_variables(enrich=["levels", "measures", "subjects"]) + +# Enrich data with unit details, attribute labels, and aggregate descriptions +data = bdl.data.get_data_by_variable( + variable_id="3643", + years=[2021], + enrich=["units", "attributes", "aggregates"], +) +``` + +Individual `enrich_*` boolean flags are also accepted (legacy style): + +``` python +data = bdl.data.get_data_by_variable("3643", years=[2021], enrich_attributes=True) +``` + +### Supported Enrichment Dimensions + +The available enrichment dimensions depend on the endpoint: + + +| Endpoint | `enrich` values | Added columns | +|----|----|----| +| Variables (`bdl.variables.*`) | `"levels"`, `"measures"`, `"subjects"` | `level_name`; `measure_unit_description`; `subject_name` | +| Data (`bdl.data.*`) | `"units"`, `"attributes"`, `"aggregates"` | `unit_name_enriched`, `unit_level`, `unit_parent_id`, `unit_kind`; `attr_name`, `attr_symbol`, `attr_description`; `aggregate_name`, `aggregate_description`, `aggregate_level` | +| Units (`bdl.units.*`) | `"levels"` | `level_name` | +| Aggregates (`bdl.aggregates.*`) | `"levels"` | `level_name` | + + +### Caching + +Lookup tables are fetched once per access-layer instance and cached for +the lifetime of the client session. Subsequent calls using the same +enrichment dimension reuse the cached data without additional API +requests. + +``` python +bdl = BDL() +# First call: fetches the levels lookup table from the API +v1 = bdl.variables.list_variables(enrich=["levels"]) +# Second call: reuses the cached levels table — no extra request +v2 = bdl.variables.get_variable("3643", enrich=["levels"]) +``` + +## Pagination + +Most list methods support pagination: + +``` python +# Fetch all pages (default, max_pages=None) +all_data = bdl.variables.list_variables() + +# Fetch only first page +first_page = bdl.variables.list_variables(max_pages=1, page_size=50) + +# Limit number of pages +limited = bdl.variables.list_variables(max_pages=5, page_size=100) +``` + +Parameters: + +- `max_pages`: Maximum number of pages to fetch. `None` (default) + fetches all pages, `1` fetches only the first page, `N` fetches up to + N pages. +- `page_size`: Number of results per page (default: 100 from config or + 100). +- `show_progress`: Display a `tqdm` progress bar while fetching pages + (default: `True`). Set to `False` to suppress output in scripts or + automated pipelines. + +``` python +# Suppress progress bar +data = bdl.data.get_data_by_variable("3643", years=[2021], show_progress=False) +``` + +## Async Usage + +All access layer methods have async versions (prefixed with `a`): + +``` python +import asyncio +from pybdl import BDL + +async def main(): + bdl = BDL() + + # Async methods return DataFrames + levels_df = await bdl.levels.alist_levels() + variables_df = await bdl.variables.alist_variables() + + # Can run multiple requests concurrently + levels_task = bdl.levels.alist_levels() + variables_task = bdl.variables.alist_variables() + + levels_df, variables_df = await asyncio.gather(levels_task, variables_task) + + return levels_df, variables_df + +asyncio.run(main()) +``` + +Available async methods: + +- `alist_levels()`, `alist_variables()`, `alist_subjects()`, etc. +- `aget_level()`, `aget_variable()`, `aget_subject()`, etc. +- `aget_data_by_variable()`, `aget_data_by_unit()`, etc. + +## Examples + +### Basic Usage + +``` python +from pybdl import BDL, BDLConfig + +bdl = BDL(BDLConfig(api_key="your-api-key")) + +# Get administrative levels +levels = bdl.levels.list_levels() +print(f"Found {len(levels)} administrative levels") + +# Get variables related to population +population_vars = bdl.variables.search_variables(name="population") +print(f"Found {len(population_vars)} population-related variables") + +# Get data for a specific variable +data = bdl.data.get_data_by_variable( + variable_id="3643", + years=[2021], + unit_level=2 # Voivodeship level +) +print(data.head()) +``` + +### Filtering and Analysis + +``` python +# Get all variables +variables = bdl.variables.list_variables() + +# Filter using pandas +economic_vars = variables[variables['name'].str.contains('economic', case=False)] + +# Get data for multiple variables +for var_id in economic_vars['id'].head(5): + data = bdl.data.get_data_by_variable(var_id, years=[2021]) + print(f"Variable {var_id}: {len(data)} records") +``` + +### Getting Data + +``` python +# Get data +df = bdl.data.get_data_by_variable("3643", years=[2021]) + +# DataFrame includes IDs and values +print(df[['unit_name', 'attr_name', 'val']].head()) + +# Group by attribute name +by_attr = df.groupby('attr_name')['val'].mean() +print(by_attr) +``` + +### Working with Nested Data + +The data endpoints automatically normalize nested structures: + +``` python +# API returns nested structure, but access layer flattens it +df = bdl.data.get_data_by_variable("3643", years=[2021]) + +# Each row represents one data point +# Columns: unit_id, unit_name, year, val, attr_id, attr_name +print(df.head()) + +# Easy to analyze +avg_by_unit = df.groupby('unit_name')['val'].mean() +print(avg_by_unit) + +# Get data for multiple years +multi_year_df = bdl.data.get_data_by_variable("3643", years=[2020, 2021, 2022]) +# Analyze trends over time +yearly_avg = multi_year_df.groupby('year')['val'].mean() +print(yearly_avg) +``` + +See [examples](examples.ipynb) for more comprehensive real-world examples. + +## API Reference + +### Module `pybdl.access.base` + +::: pybdl.access.base + +### Module `pybdl.access.enrichment` + +::: pybdl.access.enrichment + +### Module `pybdl.access.data` + +::: pybdl.access.data + +### Module `pybdl.access.variables` + +::: pybdl.access.variables + +### Module `pybdl.access.subjects` + +::: pybdl.access.subjects + +### Module `pybdl.access.units` + +::: pybdl.access.units + +### Module `pybdl.access.levels` + +::: pybdl.access.levels + +### Module `pybdl.access.measures` + +::: pybdl.access.measures + +### Module `pybdl.access.attributes` + +::: pybdl.access.attributes + +### Module `pybdl.access.aggregates` + +::: pybdl.access.aggregates + +### Module `pybdl.access.years` + +::: pybdl.access.years + +!!! seealso + +```markdown +- [Main client](main_client.md) — main client usage +- [API clients](api_clients.md) — low-level API access +- [Examples](examples.ipynb) — real-world examples +- [Configuration](config.md) — configuration options +``` diff --git a/docs/access_layer.rst b/docs/access_layer.rst deleted file mode 100644 index 2e93d88..0000000 --- a/docs/access_layer.rst +++ /dev/null @@ -1,676 +0,0 @@ -Access Layer -============= - -The access layer is the **primary user-facing interface** of pyBDL. It provides a clean, pandas DataFrame-based API that automatically handles data conversion and normalization. - -Overview --------- - -The access layer sits on top of the raw API clients and provides: - -- **Automatic DataFrame conversion**: All responses are converted to pandas DataFrames -- **Column name normalization**: camelCase API fields are converted to snake_case -- **Data type inference**: Proper types (integers, floats, booleans) are automatically detected -- **Nested data normalization**: Complex nested structures are flattened into tabular format - -The main client provides two interfaces: - -- **Access layer** (default): Returns pandas DataFrames - use `bdl.levels`, `bdl.data`, etc. -- **API layer**: Returns raw dictionaries - use `bdl.api.levels`, `bdl.api.data`, etc. - -For most users, the access layer is recommended as it provides a more Pythonic and data-analysis-friendly interface. - -Quick Start ------------ - -.. code-block:: python - - from pybdl import BDL, BDLConfig - - # Initialize client - bdl = BDL(BDLConfig(api_key="your-api-key")) - - # Access layer returns DataFrames - levels_df = bdl.levels.list_levels() - print(levels_df.head()) - - # Data is ready for analysis - print(levels_df.dtypes) - print(levels_df.columns) - -Key Features ------------- - -DataFrame Conversion -~~~~~~~~~~~~~~~~~~~~ - -All access layer methods return pandas DataFrames, making data immediately ready for analysis: - -.. code-block:: python - - # Get variables as DataFrame - variables_df = bdl.variables.list_variables() - - # Use pandas operations directly - filtered = variables_df[variables_df['name'].str.contains('population', case=False)] - sorted_vars = variables_df.sort_values('name') - -Column Name Normalization -~~~~~~~~~~~~~~~~~~~~~~~~~ - -API responses use camelCase (e.g., ``variableId``, ``unitName``), but the access layer converts these to snake_case (e.g., ``variable_id``, ``unit_name``) for Pythonic access: - -.. code-block:: python - - df = bdl.variables.get_variable("3643") - # Columns are: variable_id, name, description (not variableId, Name, Description) - print(df.columns) - -Data Type Inference -~~~~~~~~~~~~~~~~~~~ - -The access layer automatically infers and converts data types: - -.. code-block:: python - - df = bdl.data.get_data_by_variable("3643", years=[2021]) - # year column is Int64, val column is float - print(df.dtypes) - -Nested Data Normalization -~~~~~~~~~~~~~~~~~~~~~~~~~ - -The data endpoints return nested structures. The access layer automatically flattens them: - -.. code-block:: python - - # API returns: [{"id": "1", "name": "Warsaw", "values": [{"year": 2021, "val": 1000}, ...]}] - # Access layer returns flat DataFrame: - df = bdl.data.get_data_by_variable("3643", years=[2021]) - # Columns: unit_id, unit_name, year, val, attr_id - print(df.head()) - - -Available Endpoints -------------------- - -The access layer provides endpoints for all BDL API resources: - -.. list-table:: Available Access Endpoints - :header-rows: 1 - - * - Endpoint - - Access Method - - Description - * - Aggregates - - ``bdl.aggregates`` - - Aggregation level metadata - * - Attributes - - ``bdl.attributes`` - - Attribute metadata - * - Data - - ``bdl.data`` - - Statistical data access - * - Levels - - ``bdl.levels`` - - Administrative unit levels - * - Measures - - ``bdl.measures`` - - Measure unit metadata - * - Subjects - - ``bdl.subjects`` - - Subject hierarchy - * - Units - - ``bdl.units`` - - Administrative units - * - Variables - - ``bdl.variables`` - - Variable metadata - * - Years - - ``bdl.years`` - - Available years - -Endpoint Details ----------------- - -Levels -~~~~~~ - -Administrative unit aggregation levels (country, voivodeship, county, municipality): - -.. code-block:: python - - # List all levels - levels_df = bdl.levels.list_levels() - - # Get specific level - level_df = bdl.levels.get_level(1) # Level 1 = country - - # Get metadata - metadata_df = bdl.levels.get_levels_metadata() - -Subjects -~~~~~~~~ - -Subject categories and hierarchy: - -.. code-block:: python - - # List all top-level subjects - subjects_df = bdl.subjects.list_subjects() - - # Get subjects under a parent - child_subjects = bdl.subjects.list_subjects(parent_id="P0001") - - # Search subjects - results = bdl.subjects.search_subjects(name="population") - - # Get specific subject - subject_df = bdl.subjects.get_subject("P0001") - -Variables -~~~~~~~~~ - -Statistical variables (indicators): - -.. code-block:: python - - # List all variables - variables_df = bdl.variables.list_variables() - - # Filter variables - filtered = bdl.variables.list_variables( - category_id="P0001", - name="population" - ) - - # Search variables - results = bdl.variables.search_variables(name="unemployment") - - # Get specific variable - variable_df = bdl.variables.get_variable("3643") - -Data -~~~~ - -Statistical data retrieval: - -.. code-block:: python - - # Get data by variable (most common) - df = bdl.data.get_data_by_variable( - variable_id="3643", - years=[2021], - unit_level=2 # Voivodeship level - ) - - # Get data for multiple years - df = bdl.data.get_data_by_variable( - variable_id="3643", - years=[2020, 2021, 2022], - unit_level=2 - ) - - # Get data with aggregate filter - df = bdl.data.get_data_by_variable( - variable_id="3643", - years=[2021], - aggregate_id=1 - ) - - # Get data by administrative unit - df = bdl.data.get_data_by_unit( - unit_id="020000000000", - variable_ids=["3643"], - years=[2021] - ) - - # Get data for a locality - df = bdl.data.get_data_by_variable_locality( - variable_id="3643", - unit_parent_id="1465011", - years=[2021] - ) - - # Get data by unit locality - df = bdl.data.get_data_by_unit_locality( - unit_id="1465011", - variable_id="3643", - years=[2021] - ) - -The data endpoints automatically normalize nested ``values`` arrays into flat rows. - -Accessing Pagination Metadata -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Use ``return_metadata=True`` to receive a ``(DataFrame, metadata)`` tuple alongside the data. -The metadata dictionary contains information from the first response page, including pagination -details such as ``totalPages`` and ``totalRecords``. - -.. code-block:: python - - # Returns (DataFrame, metadata_dict) - df, meta = bdl.data.get_data_by_variable( - variable_id="3643", - years=[2021], - return_metadata=True, - ) - print(meta.get("totalPages")) - print(meta.get("totalRecords")) - -Convenience ``*_with_metadata`` wrappers always return a tuple: - -.. code-block:: python - - df, meta = bdl.data.get_data_by_variable_with_metadata(variable_id="3643", years=[2021]) - df, meta = bdl.data.get_data_by_unit_with_metadata(unit_id="020000000000", variable_ids=["3643"]) - df, meta = bdl.data.get_data_by_variable_locality_with_metadata( - variable_id="3643", unit_parent_id="1465011", years=[2021] - ) - df, meta = bdl.data.get_data_by_unit_locality_with_metadata( - unit_id="1465011", variable_ids=["3643"] - ) - -Units -~~~~~ - -Administrative units (regions, cities, etc.): - -.. code-block:: python - - # List units by level - voivodeships = bdl.units.list_units(level=2) # Level 2 = voivodeship - - # Search units - warsaw = bdl.units.search_units(name="Warsaw") - - # Get specific unit - unit_df = bdl.units.get_unit("020000000000") - - # List localities (statistical localities) - localities = bdl.units.list_localities(level=6) # Level 6 = municipality - - # Search localities - warsaw_localities = bdl.units.search_localities(name="Warsaw", level=6) - - # Get specific locality - locality_df = bdl.units.get_locality("1465011") - -Attributes -~~~~~~~~~~ - -Data attributes (dimensions): - -.. code-block:: python - - # List all attributes - attributes_df = bdl.attributes.list_attributes() - - # Get specific attribute - attr_df = bdl.attributes.get_attribute("1") - -Measures -~~~~~~~~ - -Measure units: - -.. code-block:: python - - # List all measures - measures_df = bdl.measures.list_measures() - - # Get specific measure - measure_df = bdl.measures.get_measure(1) - -Aggregates -~~~~~~~~~~ - -Aggregation types: - -.. code-block:: python - - # List all aggregates - aggregates_df = bdl.aggregates.list_aggregates() - - # Get specific aggregate - aggregate_df = bdl.aggregates.get_aggregate("1") - -Years -~~~~~ - -Available years for data: - -.. code-block:: python - - # List all available years - years_df = bdl.years.list_years() - - # Get specific year metadata - year_df = bdl.years.get_year(2021) - - -Enrichment ----------- - -Many access layer methods accept an ``enrich`` parameter (or individual ``enrich_*`` flags) to -automatically join human-readable reference data into the returned DataFrame. Enrichment fetches -the required lookup table once per client session, caches it in memory, and left-joins the -resolved columns onto each result row. - -Usage -~~~~~ - -Pass a list of dimension names to ``enrich``: - -.. code-block:: python - - # Enrich variables with level names, measure descriptions, and subject names - variables = bdl.variables.list_variables(enrich=["levels", "measures", "subjects"]) - - # Enrich data with unit details, attribute labels, and aggregate descriptions - data = bdl.data.get_data_by_variable( - variable_id="3643", - years=[2021], - enrich=["units", "attributes", "aggregates"], - ) - -Individual ``enrich_*`` boolean flags are also accepted (legacy style): - -.. code-block:: python - - data = bdl.data.get_data_by_variable("3643", years=[2021], enrich_attributes=True) - -Supported Enrichment Dimensions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The available enrichment dimensions depend on the endpoint: - -.. list-table:: Enrichment Support by Endpoint - :header-rows: 1 - - * - Endpoint - - ``enrich`` values - - Added columns - * - Variables (``bdl.variables.*``) - - ``"levels"``, ``"measures"``, ``"subjects"`` - - ``level_name``; ``measure_unit_description``; ``subject_name`` - * - Data (``bdl.data.*``) - - ``"units"``, ``"attributes"``, ``"aggregates"`` - - ``unit_name_enriched``, ``unit_level``, ``unit_parent_id``, ``unit_kind``; ``attr_name``, ``attr_symbol``, ``attr_description``; ``aggregate_name``, ``aggregate_description``, ``aggregate_level`` - * - Units (``bdl.units.*``) - - ``"levels"`` - - ``level_name`` - * - Aggregates (``bdl.aggregates.*``) - - ``"levels"`` - - ``level_name`` - -Caching -~~~~~~~ - -Lookup tables are fetched once per access-layer instance and cached for the lifetime of the -client session. Subsequent calls using the same enrichment dimension reuse the cached data -without additional API requests. - -.. code-block:: python - - bdl = BDL() - # First call: fetches the levels lookup table from the API - v1 = bdl.variables.list_variables(enrich=["levels"]) - # Second call: reuses the cached levels table — no extra request - v2 = bdl.variables.get_variable("3643", enrich=["levels"]) - - -Pagination ----------- - -Most list methods support pagination: - -.. code-block:: python - - # Fetch all pages (default, max_pages=None) - all_data = bdl.variables.list_variables() - - # Fetch only first page - first_page = bdl.variables.list_variables(max_pages=1, page_size=50) - - # Limit number of pages - limited = bdl.variables.list_variables(max_pages=5, page_size=100) - -Parameters: - -- ``max_pages``: Maximum number of pages to fetch. ``None`` (default) fetches all pages, ``1`` fetches only the first page, ``N`` fetches up to N pages. -- ``page_size``: Number of results per page (default: 100 from config or 100). -- ``show_progress``: Display a ``tqdm`` progress bar while fetching pages (default: ``True``). - Set to ``False`` to suppress output in scripts or automated pipelines. - -.. code-block:: python - - # Suppress progress bar - data = bdl.data.get_data_by_variable("3643", years=[2021], show_progress=False) - -Async Usage ------------ - -All access layer methods have async versions (prefixed with ``a``): - -.. code-block:: python - - import asyncio - from pybdl import BDL - - async def main(): - bdl = BDL() - - # Async methods return DataFrames - levels_df = await bdl.levels.alist_levels() - variables_df = await bdl.variables.alist_variables() - - # Can run multiple requests concurrently - levels_task = bdl.levels.alist_levels() - variables_task = bdl.variables.alist_variables() - - levels_df, variables_df = await asyncio.gather(levels_task, variables_task) - - return levels_df, variables_df - - asyncio.run(main()) - -Available async methods: - -- ``alist_levels()``, ``alist_variables()``, ``alist_subjects()``, etc. -- ``aget_level()``, ``aget_variable()``, ``aget_subject()``, etc. -- ``aget_data_by_variable()``, ``aget_data_by_unit()``, etc. - -Examples --------- - -Basic Usage -~~~~~~~~~~~ - -.. code-block:: python - - from pybdl import BDL, BDLConfig - - bdl = BDL(BDLConfig(api_key="your-api-key")) - - # Get administrative levels - levels = bdl.levels.list_levels() - print(f"Found {len(levels)} administrative levels") - - # Get variables related to population - population_vars = bdl.variables.search_variables(name="population") - print(f"Found {len(population_vars)} population-related variables") - - # Get data for a specific variable - data = bdl.data.get_data_by_variable( - variable_id="3643", - years=[2021], - unit_level=2 # Voivodeship level - ) - print(data.head()) - -Filtering and Analysis -~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: python - - # Get all variables - variables = bdl.variables.list_variables() - - # Filter using pandas - economic_vars = variables[variables['name'].str.contains('economic', case=False)] - - # Get data for multiple variables - for var_id in economic_vars['id'].head(5): - data = bdl.data.get_data_by_variable(var_id, years=[2021]) - print(f"Variable {var_id}: {len(data)} records") - -Getting Data -~~~~~~~~~~~~ - -.. code-block:: python - - # Get data - df = bdl.data.get_data_by_variable("3643", years=[2021]) - - # DataFrame includes IDs and values - print(df[['unit_name', 'attr_name', 'val']].head()) - - # Group by attribute name - by_attr = df.groupby('attr_name')['val'].mean() - print(by_attr) - -Working with Nested Data -~~~~~~~~~~~~~~~~~~~~~~~~~ - -The data endpoints automatically normalize nested structures: - -.. code-block:: python - - # API returns nested structure, but access layer flattens it - df = bdl.data.get_data_by_variable("3643", years=[2021]) - - # Each row represents one data point - # Columns: unit_id, unit_name, year, val, attr_id, attr_name - print(df.head()) - - # Easy to analyze - avg_by_unit = df.groupby('unit_name')['val'].mean() - print(avg_by_unit) - - # Get data for multiple years - multi_year_df = bdl.data.get_data_by_variable("3643", years=[2020, 2021, 2022]) - # Analyze trends over time - yearly_avg = multi_year_df.groupby('year')['val'].mean() - print(yearly_avg) - -See :doc:`examples` for more comprehensive real-world examples. - -API Reference -------------- - -Base Access -~~~~~~~~~~~ - -.. automodule:: pybdl.access.base - :members: - :undoc-members: - :show-inheritance: - :noindex: - -Enrichment -~~~~~~~~~~ - -.. automodule:: pybdl.access.enrichment - :members: - :undoc-members: - :show-inheritance: - :noindex: - -Data -~~~~ - -.. automodule:: pybdl.access.data - :members: - :undoc-members: - :show-inheritance: - :noindex: - -Variables -~~~~~~~~~ - -.. automodule:: pybdl.access.variables - :members: - :undoc-members: - :show-inheritance: - :noindex: - -Subjects -~~~~~~~~ - -.. automodule:: pybdl.access.subjects - :members: - :undoc-members: - :show-inheritance: - :noindex: - -Units -~~~~~ - -.. automodule:: pybdl.access.units - :members: - :undoc-members: - :show-inheritance: - :noindex: - -Levels -~~~~~~ - -.. automodule:: pybdl.access.levels - :members: - :undoc-members: - :show-inheritance: - :noindex: - -Measures -~~~~~~~~ - -.. automodule:: pybdl.access.measures - :members: - :undoc-members: - :show-inheritance: - :noindex: - -Attributes -~~~~~~~~~~ - -.. automodule:: pybdl.access.attributes - :members: - :undoc-members: - :show-inheritance: - :noindex: - -Aggregates -~~~~~~~~~~ - -.. automodule:: pybdl.access.aggregates - :members: - :undoc-members: - :show-inheritance: - :noindex: - -Years -~~~~~ - -.. automodule:: pybdl.access.years - :members: - :undoc-members: - :show-inheritance: - :noindex: - -.. seealso:: - - :doc:`main_client` for main client usage - - :doc:`api_clients` for low-level API access - - :doc:`examples` for real-world examples - - :doc:`config` for configuration options diff --git a/docs/api_clients.md b/docs/api_clients.md new file mode 100644 index 0000000..bd4dd74 --- /dev/null +++ b/docs/api_clients.md @@ -0,0 +1,139 @@ +# API Clients + +> [!NOTE] +> **For most users, the access layer is recommended.** This section +> documents the low-level API client interface that returns raw +> dictionaries. The access layer (documented in [access layer](access_layer.md)) returns +> pandas DataFrames and is easier to use for data analysis. +> +> Use the API layer (`bdl.api.*`) when you need: - Raw API response +> structure - Custom response processing - Direct access to API +> metadata - Integration with non-pandas workflows +> +> Use the access layer (`bdl.*`) when you need: - Pandas DataFrames for +> data analysis - Automatic column normalization and type inference - +> Nested data flattening + +The pyBDL library provides a comprehensive set of API clients for +interacting with the Local Data Bank (BDL) API. All API endpoints are +accessible through the main client's `.api` +attribute. See [Main client](main_client.md) for details about the main +client. + +| Endpoint | Class | Description | +|----|----|----| +| Aggregates | `pybdl.api.aggregates.AggregatesAPI` | Aggregation level metadata and details | +| Attributes | `pybdl.api.attributes.AttributesAPI` | Attribute metadata and details | +| Data | `pybdl.api.data.DataAPI` | Statistical data access (variables, units, localities) | +| Levels | `pybdl.api.levels.LevelsAPI` | Administrative unit aggregation levels | +| Measures | `pybdl.api.measures.MeasuresAPI` | Measure unit metadata | +| Subjects | `pybdl.api.subjects.SubjectsAPI` | Subject hierarchy and metadata | +| Units | `pybdl.api.units.UnitsAPI` | Administrative unit metadata | +| Variables | `pybdl.api.variables.VariablesAPI` | Variable metadata and details | +| Version | `pybdl.api.version.VersionAPI` | API version and build info | +| Years | `pybdl.api.years.YearsAPI` | Available years for data | + +Available API Endpoints + +> [!NOTE] +> All API clients are accessible via `bdl.api.` (e.g., +> `bdl.api.data.get_data_by_variable(...)`). + +!!! seealso + + - [Configuration](config.md) — configuration options + - [Main client](main_client.md) — main client usage + +## Async Usage + +All API clients support async methods for high-performance and +concurrent applications. Async methods are named with an +`a` prefix (e.g., +`aget_data_by_variable`). + + import asyncio + from pybdl import BDL + + async def main(): + bdl = BDL() + data = await bdl.api.data.aget_data_by_variable(variable_id="3643", years=[2021]) + print(data) + + asyncio.run(main()) + +> [!NOTE] +> Async methods are available for all endpoints. See the API reference +> below for details. + +## Format and Language Parameters + +API clients support format and language parameters for controlling +response content: + +**Format Options** (`FormatLiteral`): - `"json"` - JSON format +(default) - `"jsonapi"` - JSON:API format - `"xml"` - XML format + +**Language Options** (`LanguageLiteral`): - `"pl"` - Polish (default if +configured) - `"en"` - English + +The format and language parameters automatically set the appropriate +HTTP headers: - `Accept` header is set based on the format parameter - +`Accept-Language` header is set based on the language parameter + + from pybdl import BDL + + bdl = BDL() + + # Request data in XML format + data = bdl.api.data.get_data_by_variable( + variable_id="3643", + years=[2021], + format="xml" + ) + + # Request data in Polish + data = bdl.api.data.get_data_by_variable( + variable_id="3643", + years=[2021], + lang="pl" + ) + +### Aggregates + +::: pybdl.api.aggregates + +### Attributes + +::: pybdl.api.attributes + +### Data + +::: pybdl.api.data + +### Levels + +::: pybdl.api.levels + +### Measures + +::: pybdl.api.measures + +### Subjects + +::: pybdl.api.subjects + +### Units + +::: pybdl.api.units + +### Variables + +::: pybdl.api.variables + +### Version + +::: pybdl.api.version + +### Years + +::: pybdl.api.years diff --git a/docs/api_clients.rst b/docs/api_clients.rst deleted file mode 100644 index b473ad2..0000000 --- a/docs/api_clients.rst +++ /dev/null @@ -1,221 +0,0 @@ -API Clients -=========== - -.. note:: - **For most users, the access layer is recommended.** This section documents the low-level API client interface that returns raw dictionaries. The access layer (documented in :doc:`access_layer`) returns pandas DataFrames and is easier to use for data analysis. - - Use the API layer (``bdl.api.*``) when you need: - - Raw API response structure - - Custom response processing - - Direct access to API metadata - - Integration with non-pandas workflows - - Use the access layer (``bdl.*``) when you need: - - Pandas DataFrames for data analysis - - Automatic column normalization and type inference - - Nested data flattening - -The pyBDL library provides a comprehensive set of API clients for interacting with the Local Data Bank (BDL) API. -All API endpoints are accessible through the main client's `.api` attribute. See :doc:`Main Client ` for details about the main client. - -.. list-table:: Available API Endpoints - :header-rows: 1 - - * - Endpoint - - Class - - Description - * - Aggregates - - :class:`pybdl.api.aggregates.AggregatesAPI` - - Aggregation level metadata and details - * - Attributes - - :class:`pybdl.api.attributes.AttributesAPI` - - Attribute metadata and details - * - Data - - :class:`pybdl.api.data.DataAPI` - - Statistical data access (variables, units, localities) - * - Levels - - :class:`pybdl.api.levels.LevelsAPI` - - Administrative unit aggregation levels - * - Measures - - :class:`pybdl.api.measures.MeasuresAPI` - - Measure unit metadata - * - Subjects - - :class:`pybdl.api.subjects.SubjectsAPI` - - Subject hierarchy and metadata - * - Units - - :class:`pybdl.api.units.UnitsAPI` - - Administrative unit metadata - * - Variables - - :class:`pybdl.api.variables.VariablesAPI` - - Variable metadata and details - * - Version - - :class:`pybdl.api.version.VersionAPI` - - API version and build info - * - Years - - :class:`pybdl.api.years.YearsAPI` - - Available years for data - -.. note:: - All API clients are accessible via ``bdl.api.`` (e.g., ``bdl.api.data.get_data_by_variable(...)``). - -.. seealso:: - For configuration options, see :doc:`config`. - For main client usage, see :doc:`main_client`. - -Async Usage ------------ - -All API clients support async methods for high-performance and concurrent applications. Async methods are named with an `a` prefix (e.g., `aget_data_by_variable`). - -.. code-block:: python - - import asyncio - from pybdl import BDL - - async def main(): - bdl = BDL() - data = await bdl.api.data.aget_data_by_variable(variable_id="3643", years=[2021]) - print(data) - - asyncio.run(main()) - -.. note:: - Async methods are available for all endpoints. See the API reference below for details. - -Format and Language Parameters -------------------------------- - -API clients support format and language parameters for controlling response content: - -**Format Options** (``FormatLiteral``): -- ``"json"`` - JSON format (default) -- ``"jsonapi"`` - JSON:API format -- ``"xml"`` - XML format - -**Language Options** (``LanguageLiteral``): -- ``"pl"`` - Polish (default if configured) -- ``"en"`` - English - -The format and language parameters automatically set the appropriate HTTP headers: -- ``Accept`` header is set based on the format parameter -- ``Accept-Language`` header is set based on the language parameter - -.. code-block:: python - - from pybdl import BDL - - bdl = BDL() - - # Request data in XML format - data = bdl.api.data.get_data_by_variable( - variable_id="3643", - years=[2021], - format="xml" - ) - - # Request data in Polish - data = bdl.api.data.get_data_by_variable( - variable_id="3643", - years=[2021], - lang="pl" - ) - -Aggregates -~~~~~~~~~~ - -.. automodule:: pybdl.api.aggregates - :members: - :undoc-members: - :show-inheritance: - :inherited-members: - :noindex: - -Attributes -~~~~~~~~~~ - -.. automodule:: pybdl.api.attributes - :members: - :undoc-members: - :show-inheritance: - :inherited-members: - :noindex: - -Data -~~~~ - -.. automodule:: pybdl.api.data - :members: - :undoc-members: - :show-inheritance: - :inherited-members: - :noindex: - -Levels -~~~~~~ - -.. automodule:: pybdl.api.levels - :members: - :undoc-members: - :show-inheritance: - :inherited-members: - :noindex: - -Measures -~~~~~~~~ - -.. automodule:: pybdl.api.measures - :members: - :undoc-members: - :show-inheritance: - :inherited-members: - :noindex: - -Subjects -~~~~~~~~ - -.. automodule:: pybdl.api.subjects - :members: - :undoc-members: - :show-inheritance: - :inherited-members: - :noindex: - -Units -~~~~~ - -.. automodule:: pybdl.api.units - :members: - :undoc-members: - :show-inheritance: - :inherited-members: - :noindex: - -Variables -~~~~~~~~~ - -.. automodule:: pybdl.api.variables - :members: - :undoc-members: - :show-inheritance: - :inherited-members: - :noindex: - -Version -~~~~~~~ - -.. automodule:: pybdl.api.version - :members: - :undoc-members: - :show-inheritance: - :inherited-members: - :noindex: - -Years -~~~~~ - -.. automodule:: pybdl.api.years - :members: - :undoc-members: - :show-inheritance: - :inherited-members: - :noindex: diff --git a/docs/appendix.md b/docs/appendix.md new file mode 100644 index 0000000..9ca49ab --- /dev/null +++ b/docs/appendix.md @@ -0,0 +1,301 @@ +# Appendix: Technical Implementation Details + +This appendix contains technical implementation details for developers +and power users who need to understand the internal workings of pyBDL. +For user-facing documentation, see the main sections. + +## Rate Limiting Implementation + +### Architecture + +The rate limiting system consists of three main components: + +1. **RateLimiter**: Thread-safe synchronous rate limiter +2. **AsyncRateLimiter**: Asyncio-compatible asynchronous rate limiter +3. **PersistentQuotaCache**: Thread-safe persistent storage for quota + usage + +### Algorithm + +The rate limiter uses a **sliding window** algorithm with multiple time +periods: + +1. Each quota period maintains a deque of timestamps for recent API + calls +2. When `acquire()` is called: + - Old timestamps (outside the current window) are removed + - If current count \>= limit, calculate wait time or raise exception + - Record current timestamp for all periods + - Save state to persistent cache +3. The longest wait time across all periods is used (most restrictive + limit) + +### Time Handling + +The rate limiter uses `time.monotonic()` instead of `time.time()` to +ensure: - Clock adjustments (NTP, daylight saving) don't affect quota +calculations - Accurate elapsed time measurements - Consistent behavior +across different system clock configurations + +### Thread Safety + +- **RateLimiter**: Uses `threading.Lock()` for thread-safe operations +- **AsyncRateLimiter**: Uses `asyncio.Lock()` for async-safe operations +- **PersistentQuotaCache**: Uses `threading.Lock()` for thread-safe + cache access + +Both limiters can be safely used in concurrent environments. + +### Cache Implementation + +The persistent cache uses atomic file writes: + +1. Write quota data to a temporary file (`quota_cache.json.tmp`) +2. Atomically rename temp file to final location (`quota_cache.json`) +3. This ensures cache integrity even if the process crashes during + write + +Cache keys are unified for sync and async limiters: - Anonymous users: +`anon_` - Registered users: `reg_` + +This allows sync and async limiters to share quota state. + +### Exception Hierarchy + +``` text +GUSBDLError (base exception) +└── RateLimitError + └── RateLimitDelayExceeded +``` + +- **GUSBDLError**: Base exception for all GUS BDL API errors +- **RateLimitError**: Raised when rate limit is exceeded +- **RateLimitDelayExceeded**: Raised when required delay exceeds + `max_delay` + +### Rate Limiter Configuration Options + +RateLimiter and AsyncRateLimiter support the following parameters: + +- **quotas**: Dictionary mapping period (seconds) to limit or + (anon_limit, reg_limit) tuple +- **is_registered**: Whether the user is registered (affects quota + selection) +- **cache**: Optional PersistentQuotaCache instance for persistent + storage +- **max_delay**: Maximum seconds to wait (None = wait forever, 0 = raise + immediately) +- **raise_on_limit**: If True, raise exception immediately; if False, + wait +- **buffer_seconds**: Small buffer time added to wait calculations + (default: 0.05s) + +## Configuration Implementation Details + +### Cache File Management + +The request cache system stores responses in JSON files: + +#### Cache location + +- **Project-local** (default): `.cache/pybdl/` directory in the project + root +- **Global**: Platform-specific cache directory: + - Linux: `~/.cache/pybdl/` + - macOS: `~/Library/Caches/pybdl/` + - Windows: `%LOCALAPPDATA%\\pybdl\\cache\\` + +#### Cache file structure + +Cache files are named based on request parameters: - Format: +`{method}_{endpoint_hash}.json` - Hash includes: URL, query parameters, +headers (API key excluded) + +#### Cache expiry + +- Responses are cached with timestamps +- Expired entries are automatically ignored +- Cache files are not automatically cleaned (can be manually deleted) + +#### Internal cache helpers + +- `get_default_cache_path()`: Returns platform-appropriate cache + directory +- `get_cache_file_path(filename, use_global_cache=False, custom_path=None)`: + Returns a file path inside the resolved cache directory +- `resolve_cache_file_path(filename, use_global_cache=False, custom_file=None)`: + Resolves an explicit file path or falls back to the default cache + directory + +### Caching Internals + +pyBDL uses `hishel` on top of `httpx` for both synchronous and +asynchronous HTTP caching. + +#### HTTP client selection + +- Sync without cache: `httpx.Client` +- Sync with cache: `hishel.SyncCacheClient` +- Async without cache: `httpx.AsyncClient` +- Async with cache: `hishel.AsyncCacheClient` + +#### Cache backends + +- `cache_backend="file"`: + - Stores cached responses in `http_cache.db` + - The file lives in the same directory as the quota cache file + - Sync and async clients point to the same cache database +- `cache_backend="memory"`: + - Uses SQLite `:memory:` + - Cache is process-local and not persisted + - Sync and async clients each get their own in-memory cache +- `cache_backend=None`: + - Bypasses Hishel entirely and uses plain `httpx` clients + +#### Cache file placement + +When the file backend is enabled, pyBDL resolves the quota cache path +first and then places the HTTP cache beside it: + +``` text +/quota_cache.json +/http_cache.db +``` + +If `quota_cache_file` is explicitly set, that file's parent directory is +reused. Otherwise pyBDL uses the default project-local or global cache +directory, depending on configuration. + +#### Cache expiration model + +- `cache_expire_after` is applied as the default TTL for stored + responses +- Expired entries are treated as stale and will not be reused as fresh + cache hits +- A later request for the same URL may refresh the stored entry + +#### Quota interaction with cache + +Rate limiting and caching are intentionally coordinated: + +1. pyBDL reserves a quota slot before making a request +2. the HTTP client returns either a network response or a cached + response +3. if the response was served from cache, the reservation is released + +This design keeps quota accounting safe in mixed sync/async scenarios +while ensuring cached responses do not consume quota in normal use. + +#### Practical quota effects + +- A cache miss counts against quota +- A cache hit does not count against quota after refund +- File-backed cache can reduce repeated quota usage across separate runs +- Memory-backed cache only helps within the current process lifetime + +### Proxy Configuration Internals + +The proxy configuration is handled at the HTTP client level: + +#### Proxy stack + +- Uses `httpx.Client` / `hishel.SyncCacheClient` for synchronous + requests +- Uses `httpx.AsyncClient` / `hishel.AsyncCacheClient` for asynchronous + requests +- Proxy authentication uses HTTP Basic Auth + +#### Proxy configuration precedence + +1. Direct parameter in `BDLConfig` +2. Environment variables (`BDL_PROXY_URL`, etc.) +3. Default values (None) + +#### Supported proxy URL forms + +- HTTP proxy: `http://proxy.example.com:8080` +- HTTPS proxy: `https://proxy.example.com:8080` +- SOCKS proxy: Not directly supported (requires additional + configuration) + +#### Proxy authentication + +- Username and password are sent via HTTP Basic Auth headers +- Credentials are not logged or exposed in error messages +- For security, prefer environment variables over hardcoded credentials + +## Access Layer Implementation + +### DataFrame Conversion + +The access layer converts API responses to pandas DataFrames through +several steps: + +1. **Column Name Normalization**: camelCase → snake_case using regex + patterns +2. **Data Type Inference**: + - Attempts numeric conversion (int/float) + - Detects boolean values + - Preserves strings/objects + +### Nested Data Normalization + +For data endpoints with nested `values` arrays: + +1. Extract parent-level fields (e.g., `id`, `name`) +2. Flatten nested array: each nested item becomes a row +3. Combine parent fields with nested fields +4. Rename fields for clarity (e.g., `id` → `unit_id`) + +Example transformation: + +``` python +# API response: +[{"id": "1", "name": "Warsaw", "values": [{"year": 2021, "val": 1000}]}] + +# Access layer output: +# DataFrame with columns: unit_id, unit_name, year, val +``` + +## API Client Architecture + +### Request Handling + +All API clients inherit from a base client class that handles: + +1. **Rate Limiting**: Automatic quota enforcement before requests +2. **Caching**: Optional response caching (if enabled) +3. **Error Handling**: Converts HTTP errors to Python exceptions +4. **Pagination**: Automatic page fetching and aggregation +5. **Internationalization**: Language parameter handling + +### HTTP Client Selection + +- **Synchronous**: Uses `httpx.Client` (or `hishel.SyncCacheClient` when + caching is enabled) +- **Asynchronous**: Uses `httpx.AsyncClient` (or + `hishel.AsyncCacheClient` when caching is enabled) +- Both clients share the same configuration and rate limiting state + +### Response Processing + +1. Parse JSON response +2. Extract data array or object +3. Handle pagination metadata +4. Return structured data (dict/list) + +### Error Handling + +- HTTP 4xx/5xx errors → `GUSBDLError` or subclasses +- Rate limit errors → `RateLimitError` +- Network errors → Standard Python exceptions +- JSON parsing errors → `ValueError` + +!!! seealso + +```markdown +- [Rate limiting](rate_limiting.md) — user-facing rate limiting documentation +- [Configuration](config.md) — user-facing configuration documentation +- [API clients](api_clients.md) — API client usage +- [Access layer](access_layer.md) — access layer documentation +``` diff --git a/docs/appendix.rst b/docs/appendix.rst deleted file mode 100644 index 5a51a5f..0000000 --- a/docs/appendix.rst +++ /dev/null @@ -1,287 +0,0 @@ -Appendix: Technical Implementation Details -=========================================== - -This appendix contains technical implementation details for developers and power users who need to understand the internal workings of pyBDL. For user-facing documentation, see the main sections. - -Rate Limiting Implementation ----------------------------- - -Architecture -~~~~~~~~~~~~ - -The rate limiting system consists of three main components: - -1. **RateLimiter**: Thread-safe synchronous rate limiter -2. **AsyncRateLimiter**: Asyncio-compatible asynchronous rate limiter -3. **PersistentQuotaCache**: Thread-safe persistent storage for quota usage - -Algorithm -~~~~~~~~~ - -The rate limiter uses a **sliding window** algorithm with multiple time periods: - -1. Each quota period maintains a deque of timestamps for recent API calls -2. When ``acquire()`` is called: - - Old timestamps (outside the current window) are removed - - If current count >= limit, calculate wait time or raise exception - - Record current timestamp for all periods - - Save state to persistent cache - -3. The longest wait time across all periods is used (most restrictive limit) - -Time Handling -~~~~~~~~~~~~~ - -The rate limiter uses ``time.monotonic()`` instead of ``time.time()`` to ensure: -- Clock adjustments (NTP, daylight saving) don't affect quota calculations -- Accurate elapsed time measurements -- Consistent behavior across different system clock configurations - -Thread Safety -~~~~~~~~~~~~~ - -- **RateLimiter**: Uses ``threading.Lock()`` for thread-safe operations -- **AsyncRateLimiter**: Uses ``asyncio.Lock()`` for async-safe operations -- **PersistentQuotaCache**: Uses ``threading.Lock()`` for thread-safe cache access - -Both limiters can be safely used in concurrent environments. - -Cache Implementation -~~~~~~~~~~~~~~~~~~~~ - -The persistent cache uses atomic file writes: - -1. Write quota data to a temporary file (``quota_cache.json.tmp``) -2. Atomically rename temp file to final location (``quota_cache.json``) -3. This ensures cache integrity even if the process crashes during write - -Cache keys are unified for sync and async limiters: -- Anonymous users: ``anon_`` -- Registered users: ``reg_`` - -This allows sync and async limiters to share quota state. - -Exception Hierarchy -~~~~~~~~~~~~~~~~~~~ - -.. code-block:: text - - GUSBDLError (base exception) - └── RateLimitError - └── RateLimitDelayExceeded - -- **GUSBDLError**: Base exception for all GUS BDL API errors -- **RateLimitError**: Raised when rate limit is exceeded -- **RateLimitDelayExceeded**: Raised when required delay exceeds ``max_delay`` - -Rate Limiter Configuration Options -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -RateLimiter and AsyncRateLimiter support the following parameters: - -- **quotas**: Dictionary mapping period (seconds) to limit or (anon_limit, reg_limit) tuple -- **is_registered**: Whether the user is registered (affects quota selection) -- **cache**: Optional PersistentQuotaCache instance for persistent storage -- **max_delay**: Maximum seconds to wait (None = wait forever, 0 = raise immediately) -- **raise_on_limit**: If True, raise exception immediately; if False, wait -- **buffer_seconds**: Small buffer time added to wait calculations (default: 0.05s) - -Configuration Implementation Details ------------------------------------- - -Cache File Management -~~~~~~~~~~~~~~~~~~~~~ - -The request cache system stores responses in JSON files: - -**Cache Location** - -- **Project-local** (default): ``.cache/pybdl/`` directory in the project root -- **Global**: Platform-specific cache directory: - - Linux: ``~/.cache/pybdl/`` - - macOS: ``~/Library/Caches/pybdl/`` - - Windows: ``%LOCALAPPDATA%\\pybdl\\cache\\`` - -**Cache File Structure** - -Cache files are named based on request parameters: -- Format: ``{method}_{endpoint_hash}.json`` -- Hash includes: URL, query parameters, headers (API key excluded) - -**Cache Expiry** - -- Responses are cached with timestamps -- Expired entries are automatically ignored -- Cache files are not automatically cleaned (can be manually deleted) - -**Internal Functions** - -- ``get_default_cache_path()``: Returns platform-appropriate cache directory -- ``get_cache_file_path(filename, use_global_cache=False, custom_path=None)``: Returns a file path inside the resolved cache directory -- ``resolve_cache_file_path(filename, use_global_cache=False, custom_file=None)``: Resolves an explicit file path or falls back to the default cache directory - -Caching Internals -~~~~~~~~~~~~~~~~~ - -pyBDL uses ``hishel`` on top of ``httpx`` for both synchronous and asynchronous HTTP caching. - -**Client selection** - -- Sync without cache: ``httpx.Client`` -- Sync with cache: ``hishel.SyncCacheClient`` -- Async without cache: ``httpx.AsyncClient`` -- Async with cache: ``hishel.AsyncCacheClient`` - -**Cache backends** - -- ``cache_backend="file"``: - - Stores cached responses in ``http_cache.db`` - - The file lives in the same directory as the quota cache file - - Sync and async clients point to the same cache database -- ``cache_backend="memory"``: - - Uses SQLite ``:memory:`` - - Cache is process-local and not persisted - - Sync and async clients each get their own in-memory cache -- ``cache_backend=None``: - - Bypasses Hishel entirely and uses plain ``httpx`` clients - -**Cache file location** - -When the file backend is enabled, pyBDL resolves the quota cache path first and then places the HTTP cache beside it: - -.. code-block:: text - - /quota_cache.json - /http_cache.db - -If ``quota_cache_file`` is explicitly set, that file's parent directory is reused. Otherwise pyBDL uses the default project-local or global cache directory, depending on configuration. - -**Expiration model** - -- ``cache_expire_after`` is applied as the default TTL for stored responses -- Expired entries are treated as stale and will not be reused as fresh cache hits -- A later request for the same URL may refresh the stored entry - -**Quota interaction** - -Rate limiting and caching are intentionally coordinated: - -1. pyBDL reserves a quota slot before making a request -2. the HTTP client returns either a network response or a cached response -3. if the response was served from cache, the reservation is released - -This design keeps quota accounting safe in mixed sync/async scenarios while ensuring cached responses do not consume quota in normal use. - -**What this means in practice** - -- A cache miss counts against quota -- A cache hit does not count against quota after refund -- File-backed cache can reduce repeated quota usage across separate runs -- Memory-backed cache only helps within the current process lifetime - -Proxy Configuration Internals -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The proxy configuration is handled at the HTTP client level: - -**Implementation** - -- Uses ``httpx.Client`` / ``hishel.SyncCacheClient`` for synchronous requests -- Uses ``httpx.AsyncClient`` / ``hishel.AsyncCacheClient`` for asynchronous requests -- Proxy authentication uses HTTP Basic Auth - -**Configuration Precedence** - -1. Direct parameter in ``BDLConfig`` -2. Environment variables (``BDL_PROXY_URL``, etc.) -3. Default values (None) - -**Proxy URL Format** - -- HTTP proxy: ``http://proxy.example.com:8080`` -- HTTPS proxy: ``https://proxy.example.com:8080`` -- SOCKS proxy: Not directly supported (requires additional configuration) - -**Authentication** - -- Username and password are sent via HTTP Basic Auth headers -- Credentials are not logged or exposed in error messages -- For security, prefer environment variables over hardcoded credentials - -Access Layer Implementation ---------------------------- - -DataFrame Conversion -~~~~~~~~~~~~~~~~~~~~~ - -The access layer converts API responses to pandas DataFrames through several steps: - -1. **Column Name Normalization**: camelCase → snake_case using regex patterns -2. **Data Type Inference**: - - Attempts numeric conversion (int/float) - - Detects boolean values - - Preserves strings/objects - -Nested Data Normalization -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -For data endpoints with nested ``values`` arrays: - -1. Extract parent-level fields (e.g., ``id``, ``name``) -2. Flatten nested array: each nested item becomes a row -3. Combine parent fields with nested fields -4. Rename fields for clarity (e.g., ``id`` → ``unit_id``) - -Example transformation: - -.. code-block:: python - - # API response: - [{"id": "1", "name": "Warsaw", "values": [{"year": 2021, "val": 1000}]}] - - # Access layer output: - # DataFrame with columns: unit_id, unit_name, year, val - - -API Client Architecture ------------------------ - -Request Handling -~~~~~~~~~~~~~~~~ - -All API clients inherit from a base client class that handles: - -1. **Rate Limiting**: Automatic quota enforcement before requests -2. **Caching**: Optional response caching (if enabled) -3. **Error Handling**: Converts HTTP errors to Python exceptions -4. **Pagination**: Automatic page fetching and aggregation -5. **Internationalization**: Language parameter handling - -HTTP Client Selection -~~~~~~~~~~~~~~~~~~~~~ - -- **Synchronous**: Uses ``httpx.Client`` (or ``hishel.SyncCacheClient`` when caching is enabled) -- **Asynchronous**: Uses ``httpx.AsyncClient`` (or ``hishel.AsyncCacheClient`` when caching is enabled) -- Both clients share the same configuration and rate limiting state - -Response Processing -~~~~~~~~~~~~~~~~~~~ - -1. Parse JSON response -2. Extract data array or object -3. Handle pagination metadata -4. Return structured data (dict/list) - -Error Handling -~~~~~~~~~~~~~~~ - -- HTTP 4xx/5xx errors → ``GUSBDLError`` or subclasses -- Rate limit errors → ``RateLimitError`` -- Network errors → Standard Python exceptions -- JSON parsing errors → ``ValueError`` - -.. seealso:: - - :doc:`rate_limiting` for user-facing rate limiting documentation - - :doc:`config` for user-facing configuration documentation - - :doc:`api_clients` for API client usage - - :doc:`access_layer` for access layer documentation diff --git a/docs/changelog.md b/docs/changelog.md index 63ae71b..254f780 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,4 +1,3 @@ # Changelog -```{include} ../CHANGELOG.md -``` +{% include-markdown "../CHANGELOG.md" %} diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index ce87e88..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,41 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# For the full list of built-in configuration values, see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Project information ----------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information - -project = 'pyBDL' -copyright = '2025, Mikołaj Kaczmarek' -author = 'Mikołaj Kaczmarek' - -# -- General configuration --------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration - -extensions = ['sphinx.ext.duration', - 'sphinx.ext.doctest', - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.napoleon', - 'sphinx.ext.viewcode', - 'sphinx_autodoc_typehints', - 'myst_nb'] - -# sphinx-autodoc-typehints: show types from annotations, not in docstrings -always_document_param_types = False -typehints_fully_qualified = False -simplify_optional_unions = True - -templates_path = ['_templates'] -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -# MyST-NB configuration -nb_execution_mode = 'off' # Don't execute notebooks during build -myst_enable_extensions = ['colon_fence'] - -# -- Options for HTML output ------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output - -html_theme = 'alabaster' -html_static_path = ['_static'] diff --git a/docs/config.md b/docs/config.md new file mode 100644 index 0000000..fbf0b24 --- /dev/null +++ b/docs/config.md @@ -0,0 +1,458 @@ +# Configuration + +The `pybdl.config.BDLConfig` class manages all configuration for +authentication, language, caching, proxy settings, and quota/rate +limiting. + +## Common Configuration Scenarios + +### Basic Setup + +``` python +from pybdl import BDL, BDLConfig + +# Minimal configuration (reads API key from environment) +bdl = BDL() + +# Or provide API key directly +config = BDLConfig(api_key="your-api-key") +bdl = BDL(config) +``` + +### Anonymous Access + +The API supports anonymous access without an API key. When `api_key` is +explicitly set to `None`, the client operates in anonymous mode with +lower rate limits: + +``` python +from pybdl import BDL, BDLConfig + +# Anonymous access (explicitly None - overrides environment variables) +config = BDLConfig(api_key=None) +bdl = BDL(config) + +# Or simply pass None +bdl = BDL(config=None) # Creates default config with api_key=None + +# Or use dict +bdl = BDL(config={"api_key": None}) +``` + +**Important**: Explicitly passing `api_key=None` is stronger than +environment variables. If you want to use the environment variable +`BDL_API_KEY`, simply don't provide the `api_key` parameter (or use +`BDLConfig()`). + +**Note**: Anonymous users have lower rate limits than registered users. +See [rate limiting](rate_limiting.md) for details on quota differences. + +### Development Setup + +``` python +# Enable caching for faster development +config = BDLConfig( + api_key="your-api-key", + use_cache=True, + cache_expire_after=3600 # 1 hour +) +bdl = BDL(config) +``` + +### Production Setup + +``` python +# Production configuration with rate limiting +config = BDLConfig( + api_key="your-api-key", + use_cache=False, # Disable cache for real-time data + language="en", + quota_cache_enabled=True # Enable quota tracking +) +bdl = BDL(config) +``` + +### Corporate Network Setup + +``` python +# Behind corporate proxy +config = BDLConfig( + api_key="your-api-key", + proxy_url="http://proxy.company.com:8080", + proxy_username="username", # Or use environment variables + proxy_password="password" +) +bdl = BDL(config) +``` + +## Environment Variables + +All configuration options can be set via environment variables. Explicit +constructor arguments always take precedence over environment variables. + + +| Variable | Default | Description | +|----|----|----| +| `BDL_API_KEY` | *(none)* | API key for authenticated access. Omit for anonymous access. | +| `BDL_LANGUAGE` | `en` | Response language: `en` or `pl`. | +| `BDL_FORMAT` | `json` | Response format: `json`, `jsonapi`, or `xml`. | +| `BDL_USE_CACHE` | `true` | Enable HTTP response caching: `true` or `false`. | +| `BDL_CACHE_EXPIRY` | `3600` | Cache expiry time in seconds. | +| `BDL_PAGE_SIZE` | `100` | Default page size for paginated requests. | +| `BDL_PROXY_URL` | *(none)* | Proxy server URL, e.g. `http://proxy.example.com:8080`. | +| `BDL_PROXY_USERNAME` | *(none)* | Username for proxy authentication. | +| `BDL_PROXY_PASSWORD` | *(none)* | Password for proxy authentication. | +| `BDL_REQUEST_RETRIES` | `3` | Number of retry attempts for transient HTTP errors. | +| `BDL_RETRY_BACKOFF_FACTOR` | `0.5` | Base backoff multiplier (seconds) between retries. | +| `BDL_MAX_RETRY_DELAY` | `30.0` | Maximum time in seconds to wait between retries. | +| `BDL_RETRY_STATUS_CODES` | `429,500,502,503,504` | Comma-separated HTTP status codes that trigger a retry. | +| `BDL_RATE_LIMIT_RAISE` | `false` | If `true`, raise `RateLimitError` when client-side quota is exhausted; if `false` (default), wait until a slot is available. | +| `BDL_HTTP_429_MAX_RETRIES` | `12` | Max retries when the **server** returns HTTP 429 (separate from `BDL_REQUEST_RETRIES` for 5xx). Honors `Retry-After` up to `BDL_HTTP_429_MAX_DELAY`; otherwise uses exponential backoff from `BDL_RETRY_BACKOFF_FACTOR`. | +| `BDL_HTTP_429_MAX_DELAY` | `900` | Max seconds to wait between HTTP 429 retries (15 minutes; aligns with common BDL quota windows). | +| `BDL_QUOTAS` | *(BDL defaults)* | JSON object overriding rate-limit quotas, e.g. `'{"1": 20, "900": 500}'`. | +| `BDL_QUOTA_CACHE_ENABLED` | `true` | Persist quota usage across process restarts. | +| `BDL_QUOTA_CACHE` | *(auto)* | Path to the quota cache file. | +| `BDL_USE_GLOBAL_CACHE` | `false` | Store quota cache in the OS-level cache directory instead of the project `.cache/`. | + + +Example — set environment variables and use defaults in code: + +``` bash +export BDL_API_KEY="your-api-key" +export BDL_LANGUAGE="en" +export BDL_USE_CACHE="true" +export BDL_CACHE_EXPIRY="3600" +export BDL_PROXY_URL="http://proxy.example.com:8080" +export BDL_PROXY_USERNAME="user" +export BDL_PROXY_PASSWORD="pass" +export BDL_QUOTAS='{"1": 20, "900": 500}' +``` + +``` python +# All settings are read from environment variables +bdl = BDL() +``` + +!!! seealso + +```markdown +- [Main client](main_client.md) — main client usage +- [Access layer](access_layer.md) — access layer usage +- [API clients](api_clients.md) — API endpoint usage +- [Rate limiting](rate_limiting.md) — comprehensive rate limiting documentation +``` + +## Caching + +pyBDL supports transparent HTTP response caching to speed up repeated +queries and reduce unnecessary API traffic. The same caching model is +available for both synchronous and asynchronous clients, so repeated +calls made through either interface can reuse previously stored +responses. + +At a high level, caching works like this: + +1. The first request for a given URL is sent to the BDL API and the + response is stored. +2. A later request for the same URL can be served from cache instead of + making another network call. +3. Cached responses expire after `cache_expire_after` seconds. +4. When the response comes from cache, pyBDL refunds the temporary + quota reservation, so cached reads do not consume rate limit quota. + +### Caching basic usage + +``` python +# Enable file-backed caching with 10-minute expiry +config = BDLConfig(api_key="...", cache_backend="file", cache_expire_after=600) +bdl = BDL(config) + +# First call hits the API +data1 = bdl.data.get_data_by_variable("3643", years=[2021]) + +# Second call uses cache (if within expiry time) +data2 = bdl.data.get_data_by_variable("3643", years=[2021]) +``` + +### Caching backends + +pyBDL supports two cache backends plus a disabled mode: + +- `cache_backend="file"`: Stores cache data on disk and is the + recommended default for most users. +- `cache_backend="memory"`: Uses an in-memory SQLite database that + exists only for the lifetime of the current process. +- `cache_backend=None`: Disables caching completely. + +``` python +from pybdl import BDL, BDLConfig + +# File-backed cache shared across sync/async clients +file_config = BDLConfig( + api_key="...", + cache_backend="file", + cache_expire_after=3600, +) + +# In-memory cache for short-lived scripts or tests +memory_config = BDLConfig( + api_key="...", + cache_backend="memory", + cache_expire_after=300, +) + +# Disable cache entirely +no_cache_config = BDLConfig( + api_key="...", + cache_backend=None, +) +``` + +### Caching configuration fields + +- `use_cache`: Backward-compatible boolean toggle for caching +- `cache_backend`: `"file"`, `"memory"`, or `None` to disable caching +- `cache_expire_after`: Cache expiry time in seconds (default: 3600 = 1 + hour) + +### Caching behavior + +- The first request for a resource is usually slower because it goes to + the network. +- Repeated requests for the same URL are usually faster because the + cached response can be reused. +- File-backed cache persists across process restarts. +- Memory-backed cache is cleared when the Python process exits. +- With `cache_backend="file"`, sync and async clients use the same cache + file and can reuse each other's cached responses. +- With `cache_backend="memory"`, sync and async clients each keep their + own in-memory cache and do not share entries. +- Cache keys are based on the actual HTTP request, so changing endpoint + parameters, language, format, or headers that affect representation + may produce a different cache entry. + +### Caching and rate limiting + +pyBDL still reserves a quota slot before issuing the request, because at +that moment it does not yet know whether the response will come from +cache. If the response is later identified as a cache hit, that +reservation is released immediately. + +In practice this means: + +- Real network requests count against quota. +- Cache hits do not reduce available quota. +- You can safely enable caching to reduce rate-limit pressure during + repeated exploration or batch workflows. + +### Choosing a cache backend + +Use `"file"` when: + +- You want cache reuse between script runs +- You mix sync and async usage and want both to share cache entries +- You do longer exploratory or ETL-style workflows + +Use `"memory"` when: + +- You want temporary caching only inside one process +- You do not want cache files on disk +- You are running isolated tests or short-lived scripts + +Use `None` when: + +- You always want fresh data from the API +- You are debugging live responses +- You want the simplest possible request behavior + +### When caching helps + +- Development and testing: Speed up repeated queries +- Data exploration: Avoid re-fetching the same data +- Batch processing: Cache metadata queries + +### When not to use caching + +- Real-time data: When you need the latest data +- One-off scripts: No benefit if queries aren't repeated +- Memory-constrained environments: Cache uses disk space + +### Caching environment variables + +Caching can also be configured through environment variables: + +``` bash +export BDL_CACHE_BACKEND="file" +export BDL_CACHE_EXPIRY="1800" +``` + +``` python +# Reads cache settings from the environment +bdl = BDL(BDLConfig(api_key="...")) +``` + +For technical details about cache file management, cache locations, and +implementation details, see [appendix](appendix.md). + +## Proxy Configuration + +The library supports HTTP/HTTPS proxy configuration for environments +behind corporate firewalls or proxies. + +### Proxy basic setup + +``` python +# Direct configuration +config = BDLConfig( + api_key="your-api-key", + proxy_url="http://proxy.example.com:8080", + proxy_username="user", # Optional + proxy_password="pass" # Optional +) +bdl = BDL(config) +``` + +### Proxy credentials via environment + +For security, prefer environment variables over hardcoded credentials: + +``` bash +export BDL_PROXY_URL="http://proxy.example.com:8080" +export BDL_PROXY_USERNAME="user" +export BDL_PROXY_PASSWORD="pass" +``` + +Then use in Python: + +``` python +# Configuration is read from environment variables +config = BDLConfig(api_key="your-api-key") +bdl = BDL(config) +``` + +### Proxy configuration precedence + +Settings are applied in this order: 1. Direct parameter passing (highest +priority) 2. Environment variables 3. Default values (None) + +### Proxy common scenarios + +- Corporate networks requiring proxy access +- VPN connections +- Development environments behind firewalls + +For technical details about proxy implementation, see [appendix](appendix.md). + +## Rate Limiting & Quotas + +pyBDL enforces API rate limits using both synchronous and asynchronous +rate limiters. These limits are based on the official BDL API provider's +policy, as described in the [BDL API +Manual](https://api.stat.gov.pl/Home/BdlApi) (see the "Manual" tab). + +### Rate limiting overview + +- **Automatic enforcement**: Rate limiting is built into all API calls +- **Multiple quota periods**: Enforces limits across different time + windows simultaneously +- **Persistent cache**: Quota usage survives process restarts +- **Sync & async support**: Works seamlessly with both synchronous and + asynchronous code +- **Wait by default**: `raise_on_rate_limit` defaults to `False` so the + client waits for quota; set `True` or `BDL_RATE_LIMIT_RAISE` to raise + immediately (see [rate limiting](rate_limiting.md)) + +### Default quotas + +The following user limits apply. Quotas are automatically selected based +on whether `api_key` is provided: + +| Period | Anonymous user | Registered user | +|--------|----------------|-----------------| +| 1s | 5 | 10 | +| 15m | 100 | 500 | +| 12h | 1,000 | 5,000 | +| 7d | 10,000 | 50,000 | + +- **Anonymous user**: `api_key=None` or not provided (no `X-ClientId` + header sent) +- **Registered user**: `api_key` is provided (`X-ClientId` header sent + with API key) + +The rate limiter automatically selects the appropriate quota limits +based on registration status. + +### Custom quota overrides + +To override default rate limits, provide a +`custom_quotas` dictionary with integer +keys representing the period in seconds: + +``` python +config = BDLConfig( + api_key="...", + custom_quotas={1: 10, 900: 200, 43200: 2000, 604800: 20000}, +) +``` + +Or via environment variable: + +``` bash +export BDL_QUOTAS='{"1": 20, "900": 500}' +``` + +### Further rate limiting documentation + +For detailed information on rate limiting (errors, wait vs. raise, context +managers, decorators, remaining quota, implementation details), see +[rate limiting](rate_limiting.md). + +## Retry Configuration + +pyBDL automatically retries requests that fail with transient HTTP +errors. Retries use exponential back-off up to a configurable ceiling. + +### Retry options + +- `request_retries`: Total retry attempts before raising (default: `3`). +- `retry_backoff_factor`: Back-off multiplier in seconds (default: + `0.5`). Delay before attempt *n* = `retry_backoff_factor × 2^(n-1)` + seconds. +- `max_retry_delay`: Upper bound on any individual retry delay in + seconds (default: `30.0`). +- `retry_status_codes`: HTTP status codes that should trigger a retry + (default: `429, 500, 502, 503, 504`). + +### Retry examples + +``` python +from pybdl import BDL, BDLConfig + +# Aggressive retry for unreliable networks +config = BDLConfig( + api_key="your-api-key", + request_retries=5, + retry_backoff_factor=1.0, + max_retry_delay=60.0, +) +bdl = BDL(config) +``` + +``` python +# Disable retries entirely +config = BDLConfig(api_key="your-api-key", request_retries=0) +bdl = BDL(config) +``` + +Or via environment variables: + +``` bash +export BDL_REQUEST_RETRIES=5 +export BDL_RETRY_BACKOFF_FACTOR=1.0 +export BDL_MAX_RETRY_DELAY=60.0 +export BDL_RETRY_STATUS_CODES="429,500,502,503,504" +``` + +## API reference + +::: pybdl.config diff --git a/docs/config.rst b/docs/config.rst deleted file mode 100644 index d70f408..0000000 --- a/docs/config.rst +++ /dev/null @@ -1,477 +0,0 @@ -Configuration -============= - -.. automodule:: pybdl.config - :members: - :undoc-members: - :show-inheritance: - :inherited-members: - :noindex: - -The :class:`pybdl.config.BDLConfig` class manages all configuration for authentication, language, caching, proxy settings, and quota/rate limiting. - -Common Configuration Scenarios -------------------------------- - -Basic Setup -~~~~~~~~~~~ - -.. code-block:: python - - from pybdl import BDL, BDLConfig - - # Minimal configuration (reads API key from environment) - bdl = BDL() - - # Or provide API key directly - config = BDLConfig(api_key="your-api-key") - bdl = BDL(config) - -Anonymous Access -~~~~~~~~~~~~~~~~ - -The API supports anonymous access without an API key. When ``api_key`` is explicitly set to ``None``, the client operates in anonymous mode with lower rate limits: - -.. code-block:: python - - from pybdl import BDL, BDLConfig - - # Anonymous access (explicitly None - overrides environment variables) - config = BDLConfig(api_key=None) - bdl = BDL(config) - - # Or simply pass None - bdl = BDL(config=None) # Creates default config with api_key=None - - # Or use dict - bdl = BDL(config={"api_key": None}) - -**Important**: Explicitly passing ``api_key=None`` is stronger than environment variables. If you want to use the environment variable ``BDL_API_KEY``, simply don't provide the ``api_key`` parameter (or use ``BDLConfig()``). - -**Note**: Anonymous users have lower rate limits than registered users. See :doc:`rate_limiting` for details on quota differences. - -Development Setup -~~~~~~~~~~~~~~~~~ - -.. code-block:: python - - # Enable caching for faster development - config = BDLConfig( - api_key="your-api-key", - use_cache=True, - cache_expire_after=3600 # 1 hour - ) - bdl = BDL(config) - -Production Setup -~~~~~~~~~~~~~~~~ - -.. code-block:: python - - # Production configuration with rate limiting - config = BDLConfig( - api_key="your-api-key", - use_cache=False, # Disable cache for real-time data - language="en", - quota_cache_enabled=True # Enable quota tracking - ) - bdl = BDL(config) - -Corporate Network Setup -~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: python - - # Behind corporate proxy - config = BDLConfig( - api_key="your-api-key", - proxy_url="http://proxy.company.com:8080", - proxy_username="username", # Or use environment variables - proxy_password="password" - ) - bdl = BDL(config) - -Environment Variables ---------------------- - -All configuration options can be set via environment variables. Explicit constructor arguments -always take precedence over environment variables. - -.. list-table:: Complete Environment Variable Reference - :header-rows: 1 - :widths: 30 20 50 - - * - Variable - - Default - - Description - * - ``BDL_API_KEY`` - - *(none)* - - API key for authenticated access. Omit for anonymous access. - * - ``BDL_LANGUAGE`` - - ``en`` - - Response language: ``en`` or ``pl``. - * - ``BDL_FORMAT`` - - ``json`` - - Response format: ``json``, ``jsonapi``, or ``xml``. - * - ``BDL_USE_CACHE`` - - ``true`` - - Enable HTTP response caching: ``true`` or ``false``. - * - ``BDL_CACHE_EXPIRY`` - - ``3600`` - - Cache expiry time in seconds. - * - ``BDL_PAGE_SIZE`` - - ``100`` - - Default page size for paginated requests. - * - ``BDL_PROXY_URL`` - - *(none)* - - Proxy server URL, e.g. ``http://proxy.example.com:8080``. - * - ``BDL_PROXY_USERNAME`` - - *(none)* - - Username for proxy authentication. - * - ``BDL_PROXY_PASSWORD`` - - *(none)* - - Password for proxy authentication. - * - ``BDL_REQUEST_RETRIES`` - - ``3`` - - Number of retry attempts for transient HTTP errors. - * - ``BDL_RETRY_BACKOFF_FACTOR`` - - ``0.5`` - - Base backoff multiplier (seconds) between retries. - * - ``BDL_MAX_RETRY_DELAY`` - - ``30.0`` - - Maximum time in seconds to wait between retries. - * - ``BDL_RETRY_STATUS_CODES`` - - ``429,500,502,503,504`` - - Comma-separated HTTP status codes that trigger a retry. - * - ``BDL_RATE_LIMIT_RAISE`` - - ``false`` - - If ``true``, raise ``RateLimitError`` when client-side quota is exhausted; if ``false`` (default), wait until a slot is available. - * - ``BDL_HTTP_429_MAX_RETRIES`` - - ``12`` - - Max retries when the **server** returns HTTP 429 (separate from ``BDL_REQUEST_RETRIES`` for 5xx). Honors ``Retry-After`` up to ``BDL_HTTP_429_MAX_DELAY``; otherwise uses exponential backoff from ``BDL_RETRY_BACKOFF_FACTOR``. - * - ``BDL_HTTP_429_MAX_DELAY`` - - ``900`` - - Max seconds to wait between HTTP 429 retries (15 minutes; aligns with common BDL quota windows). - * - ``BDL_QUOTAS`` - - *(BDL defaults)* - - JSON object overriding rate-limit quotas, e.g. ``'{"1": 20, "900": 500}'``. - * - ``BDL_QUOTA_CACHE_ENABLED`` - - ``true`` - - Persist quota usage across process restarts. - * - ``BDL_QUOTA_CACHE`` - - *(auto)* - - Path to the quota cache file. - * - ``BDL_USE_GLOBAL_CACHE`` - - ``false`` - - Store quota cache in the OS-level cache directory instead of the project ``.cache/``. - -Example — set environment variables and use defaults in code: - -.. code-block:: bash - - export BDL_API_KEY="your-api-key" - export BDL_LANGUAGE="en" - export BDL_USE_CACHE="true" - export BDL_CACHE_EXPIRY="3600" - export BDL_PROXY_URL="http://proxy.example.com:8080" - export BDL_PROXY_USERNAME="user" - export BDL_PROXY_PASSWORD="pass" - export BDL_QUOTAS='{"1": 20, "900": 500}' - -.. code-block:: python - - # All settings are read from environment variables - bdl = BDL() - -.. seealso:: - - :doc:`main_client` for main client usage - - :doc:`access_layer` for access layer usage - - :doc:`api_clients` for API endpoint usage - - :doc:`rate_limiting` for comprehensive rate limiting documentation - -Caching -------- - -pyBDL supports transparent HTTP response caching to speed up repeated queries and reduce unnecessary API traffic. The same caching model is available for both synchronous and asynchronous clients, so repeated calls made through either interface can reuse previously stored responses. - -At a high level, caching works like this: - -1. The first request for a given URL is sent to the BDL API and the response is stored. -2. A later request for the same URL can be served from cache instead of making another network call. -3. Cached responses expire after ``cache_expire_after`` seconds. -4. When the response comes from cache, pyBDL refunds the temporary quota reservation, so cached reads do not consume rate limit quota. - -**Basic Usage** - -.. code-block:: python - - # Enable file-backed caching with 10-minute expiry - config = BDLConfig(api_key="...", cache_backend="file", cache_expire_after=600) - bdl = BDL(config) - - # First call hits the API - data1 = bdl.data.get_data_by_variable("3643", years=[2021]) - - # Second call uses cache (if within expiry time) - data2 = bdl.data.get_data_by_variable("3643", years=[2021]) - -**Backend Options** - -pyBDL supports two cache backends plus a disabled mode: - -- ``cache_backend="file"``: Stores cache data on disk and is the recommended default for most users. -- ``cache_backend="memory"``: Uses an in-memory SQLite database that exists only for the lifetime of the current process. -- ``cache_backend=None``: Disables caching completely. - -.. code-block:: python - - from pybdl import BDL, BDLConfig - - # File-backed cache shared across sync/async clients - file_config = BDLConfig( - api_key="...", - cache_backend="file", - cache_expire_after=3600, - ) - - # In-memory cache for short-lived scripts or tests - memory_config = BDLConfig( - api_key="...", - cache_backend="memory", - cache_expire_after=300, - ) - - # Disable cache entirely - no_cache_config = BDLConfig( - api_key="...", - cache_backend=None, - ) - -**Configuration Options** - -- ``use_cache``: Backward-compatible boolean toggle for caching -- ``cache_backend``: ``"file"``, ``"memory"``, or ``None`` to disable caching -- ``cache_expire_after``: Cache expiry time in seconds (default: 3600 = 1 hour) - -**What to Expect** - -- The first request for a resource is usually slower because it goes to the network. -- Repeated requests for the same URL are usually faster because the cached response can be reused. -- File-backed cache persists across process restarts. -- Memory-backed cache is cleared when the Python process exits. -- With ``cache_backend="file"``, sync and async clients use the same cache file and can reuse each other's cached responses. -- With ``cache_backend="memory"``, sync and async clients each keep their own in-memory cache and do not share entries. -- Cache keys are based on the actual HTTP request, so changing endpoint parameters, language, format, or headers that affect representation may produce a different cache entry. - -**Rate Limiting Interaction** - -pyBDL still reserves a quota slot before issuing the request, because at that moment it does not yet know whether the response will come from cache. If the response is later identified as a cache hit, that reservation is released immediately. - -In practice this means: - -- Real network requests count against quota. -- Cache hits do not reduce available quota. -- You can safely enable caching to reduce rate-limit pressure during repeated exploration or batch workflows. - -**Choosing a Backend** - -Use ``"file"`` when: - -- You want cache reuse between script runs -- You mix sync and async usage and want both to share cache entries -- You do longer exploratory or ETL-style workflows - -Use ``"memory"`` when: - -- You want temporary caching only inside one process -- You do not want cache files on disk -- You are running isolated tests or short-lived scripts - -Use ``None`` when: - -- You always want fresh data from the API -- You are debugging live responses -- You want the simplest possible request behavior - -**When to Use Caching** - -- Development and testing: Speed up repeated queries -- Data exploration: Avoid re-fetching the same data -- Batch processing: Cache metadata queries - -**When Not to Use Caching** - -- Real-time data: When you need the latest data -- One-off scripts: No benefit if queries aren't repeated -- Memory-constrained environments: Cache uses disk space - -**Environment Variables** - -Caching can also be configured through environment variables: - -.. code-block:: bash - - export BDL_CACHE_BACKEND="file" - export BDL_CACHE_EXPIRY="1800" - -.. code-block:: python - - # Reads cache settings from the environment - bdl = BDL(BDLConfig(api_key="...")) - -For technical details about cache file management, cache locations, and implementation details, see :doc:`appendix`. - -Proxy Configuration -------------------- - -The library supports HTTP/HTTPS proxy configuration for environments behind corporate firewalls or proxies. - -**Basic Configuration** - -.. code-block:: python - - # Direct configuration - config = BDLConfig( - api_key="your-api-key", - proxy_url="http://proxy.example.com:8080", - proxy_username="user", # Optional - proxy_password="pass" # Optional - ) - bdl = BDL(config) - -**Using Environment Variables** - -For security, prefer environment variables over hardcoded credentials: - -.. code-block:: bash - - export BDL_PROXY_URL="http://proxy.example.com:8080" - export BDL_PROXY_USERNAME="user" - export BDL_PROXY_PASSWORD="pass" - -Then use in Python: - -.. code-block:: python - - # Configuration is read from environment variables - config = BDLConfig(api_key="your-api-key") - bdl = BDL(config) - -**Configuration Precedence** - -Settings are applied in this order: -1. Direct parameter passing (highest priority) -2. Environment variables -3. Default values (None) - -**Common Use Cases** - -- Corporate networks requiring proxy access -- VPN connections -- Development environments behind firewalls - -For technical details about proxy implementation, see :doc:`appendix`. - -Rate Limiting & Quotas ----------------------- - -pyBDL enforces API rate limits using both synchronous and asynchronous rate limiters. These limits are based on the official BDL API provider's policy, as described in the `BDL API Manual `_ (see the "Manual" tab). - -**Quick Overview** - -- **Automatic enforcement**: Rate limiting is built into all API calls -- **Multiple quota periods**: Enforces limits across different time windows simultaneously -- **Persistent cache**: Quota usage survives process restarts -- **Sync & async support**: Works seamlessly with both synchronous and asynchronous code -- **Wait by default**: ``raise_on_rate_limit`` defaults to ``False`` so the client waits for quota; set ``True`` or ``BDL_RATE_LIMIT_RAISE`` to raise immediately (see :doc:`rate_limiting`) - -**Default Quotas** - -The following user limits apply. Quotas are automatically selected based on whether ``api_key`` is provided: - -+---------+------------------+-------------------+ -| Period | Anonymous user | Registered user | -+=========+==================+===================+ -| 1s | 5 | 10 | -+---------+------------------+-------------------+ -| 15m | 100 | 500 | -+---------+------------------+-------------------+ -| 12h | 1,000 | 5,000 | -+---------+------------------+-------------------+ -| 7d | 10,000 | 50,000 | -+---------+------------------+-------------------+ - -- **Anonymous user**: ``api_key=None`` or not provided (no ``X-ClientId`` header sent) -- **Registered user**: ``api_key`` is provided (``X-ClientId`` header sent with API key) - -The rate limiter automatically selects the appropriate quota limits based on registration status. - -**Custom Quotas** - -To override default rate limits, provide a `custom_quotas` dictionary with integer keys representing the period in seconds: - -.. code-block:: python - - config = BDLConfig(api_key="...", custom_quotas={1: 10, 900: 200, 43200: 2000, 604800: 20000}) - -Or via environment variable: - -.. code-block:: bash - - export BDL_QUOTAS='{"1": 20, "900": 500}' - -**Comprehensive Documentation** - -For detailed information on rate limiting, including: -- How to handle rate limit errors -- Configuring wait vs. raise behavior -- Using context managers and decorators -- Checking remaining quota -- Technical implementation details - -See :doc:`rate_limiting` for the complete guide. - -Retry Configuration -------------------- - -pyBDL automatically retries requests that fail with transient HTTP errors. Retries use -exponential back-off up to a configurable ceiling. - -**Configuration Options** - -- ``request_retries``: Total retry attempts before raising (default: ``3``). -- ``retry_backoff_factor``: Back-off multiplier in seconds (default: ``0.5``). - Delay before attempt *n* = ``retry_backoff_factor × 2^(n-1)`` seconds. -- ``max_retry_delay``: Upper bound on any individual retry delay in seconds (default: ``30.0``). -- ``retry_status_codes``: HTTP status codes that should trigger a retry - (default: ``429, 500, 502, 503, 504``). - -**Examples** - -.. code-block:: python - - from pybdl import BDL, BDLConfig - - # Aggressive retry for unreliable networks - config = BDLConfig( - api_key="your-api-key", - request_retries=5, - retry_backoff_factor=1.0, - max_retry_delay=60.0, - ) - bdl = BDL(config) - -.. code-block:: python - - # Disable retries entirely - config = BDLConfig(api_key="your-api-key", request_retries=0) - bdl = BDL(config) - -Or via environment variables: - -.. code-block:: bash - - export BDL_REQUEST_RETRIES=5 - export BDL_RETRY_BACKOFF_FACTOR=1.0 - export BDL_MAX_RETRY_DELAY=60.0 - export BDL_RETRY_STATUS_CODES="429,500,502,503,504" diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..861dd51 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,90 @@ +# pyBDL documentation + +## What is the Local Data Bank (BDL)? + +The Local Data Bank (BDL, Bank Danych Lokalnych) is Poland's official +statistical data warehouse, maintained by Statistics Poland (GUS). It +provides access to a vast range of statistical indicators and datasets +covering: + +- Demographics and population +- Economy and labor market +- Education, health, and social welfare +- Environment and infrastructure +- Regional and local statistics (down to municipality level) +- Historical time series and more + +Data is available for various administrative units (country, +voivodeship, county, municipality) and can be filtered by year, subject, +and other attributes. The BDL is a primary source for open, official +statistics in Poland. + +For a full description of available data, endpoints, and API usage, see: + +- Official BDL API documentation: +- BDL web portal: + +pyBDL is a modern, Pythonic client library for the Local Data Bank (BDL, +Bank Danych Lokalnych) API, enabling easy, robust access to Polish +official statistics for data science, research, and applications. + +## Features + +- Clean, modular API client for all BDL endpoints +- Pandas DataFrame integration for tabular data +- Full support for pagination, filtering, and internationalization +- Built-in API key, language, and cache configuration +- Open source, tested, and ready for data analysis and visualization + +## Quick Start + +``` python +from pybdl import BDL, BDLConfig + +# Initialize client (reads config from environment or defaults) +bdl = BDL(BDLConfig(api_key="your-api-key")) + +# Use the access layer (returns pandas DataFrames) +df = bdl.data.get_data_by_variable(variable_id="3643", years=[2021]) +print(df.head()) + +# Data is ready for analysis +print(df.dtypes) +print(df.columns) +``` + +## Configuration + +Configure your API key and options via environment variables or +directly: + +``` python +from pybdl import BDLConfig +config = BDLConfig(api_key="your-api-key", language="en", use_cache=True) +bdl = BDL(config=config) +``` + +Or set environment variables: + +```bash +export BDL_API_KEY=your-api-key +export BDL_LANGUAGE=en +``` + +## Documentation + +Use the navigation tabs for the full guide. Highlights: + +- [Main client](main_client.md) — `BDL` entry point and sync/async usage +- [Access layer](access_layer.md) — DataFrame-based API +- [Examples](examples.ipynb) — Jupyter notebook walkthrough +- [API clients](api_clients.md) — low-level dictionary API +- [Configuration](config.md) — `BDLConfig`, environment variables, caching +- [Rate limiting](rate_limiting.md) — quotas and behavior +- [Appendix](appendix.md) — implementation notes + +## Contributing & License + +pyBDL is open source under the MIT license. Contributions and issues are +welcome! For details, see the [GitHub +repository](https://github.com/AN0DA/pybdl). diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 8fe7330..0000000 --- a/docs/index.rst +++ /dev/null @@ -1,95 +0,0 @@ -pyBDL documentation -=================== - -What is the Local Data Bank (BDL)? ----------------------------------- - -The Local Data Bank (BDL, Bank Danych Lokalnych) is Poland's official statistical data warehouse, maintained by Statistics Poland (GUS). It provides access to a vast range of statistical indicators and datasets covering: - -- Demographics and population -- Economy and labor market -- Education, health, and social welfare -- Environment and infrastructure -- Regional and local statistics (down to municipality level) -- Historical time series and more - -Data is available for various administrative units (country, voivodeship, county, municipality) and can be filtered by year, subject, and other attributes. The BDL is a primary source for open, official statistics in Poland. - -For a full description of available data, endpoints, and API usage, see: - -- Official BDL API documentation: https://api.stat.gov.pl/Home/BdlApi -- BDL web portal: https://bdl.stat.gov.pl/bdl/start - -pyBDL is a modern, Pythonic client library for the Local Data Bank (BDL, Bank Danych Lokalnych) API, -enabling easy, robust access to Polish official statistics for data science, research, -and applications. - -Features --------- - -- Clean, modular API client for all BDL endpoints -- Pandas DataFrame integration for tabular data -- Full support for pagination, filtering, and internationalization -- Built-in API key, language, and cache configuration -- Open source, tested, and ready for data analysis and visualization - -Quick Start ------------ - -.. code-block:: python - - from pybdl import BDL, BDLConfig - - # Initialize client - bdl = BDL(BDLConfig(api_key="your-api-key")) # Reads config from environment or defaults - - # Use the access layer (returns pandas DataFrames) - df = bdl.data.get_data_by_variable(variable_id="3643", years=[2021]) - print(df.head()) - - # Data is ready for analysis - print(df.dtypes) - print(df.columns) - -Configuration -------------- - -Configure your API key and options via environment variables or directly: - -.. code-block:: python - - from pybdl import BDLConfig - config = BDLConfig(api_key="your-api-key", language="en", use_cache=True) - bdl = BDL(config=config) - -Or set environment variables:: - - export BDL_API_KEY=your-api-key - export BDL_LANGUAGE=en - -Documentation -------------- - -.. toctree:: - :maxdepth: 2 - :caption: User Guide - - main_client - access_layer - examples - api_clients - config - rate_limiting - -.. toctree:: - :maxdepth: 2 - :caption: Reference - - appendix - changelog.md - -Contributing & License ----------------------- - -pyBDL is open source under the MIT license. Contributions and issues are welcome! -For details, see the `GitHub repository `_. diff --git a/docs/main_client.md b/docs/main_client.md new file mode 100644 index 0000000..12cd342 --- /dev/null +++ b/docs/main_client.md @@ -0,0 +1,193 @@ +# Main Client + +The `pybdl.client.BDL` class is the main entry point for the library. It +provides **two interfaces** for accessing BDL data: + +1. **Access Layer** (default, recommended): Returns pandas DataFrames - + use `bdl.levels`, `bdl.data`, etc. +2. **API Layer**: Returns raw dictionaries - use `bdl.api.levels`, + `bdl.api.data`, etc. + +For most users, the **access layer is recommended** as it provides +DataFrames that are immediately ready for data analysis. + +## Access Layer (Default Interface) + +The access layer is the primary interface and returns pandas DataFrames: + +``` python +from pybdl import BDL, BDLConfig + +bdl = BDL(BDLConfig(api_key="your-api-key")) + +# Access layer - returns DataFrames +levels_df = bdl.levels.list_levels() +variables_df = bdl.variables.list_variables() +data_df = bdl.data.get_data_by_variable(variable_id="3643", years=[2021]) + +# Data is ready for pandas operations +print(levels_df.head()) +print(data_df.dtypes) +print(data_df.columns) +``` + +Key features of the access layer: + +- **Automatic DataFrame conversion**: All responses are pandas + DataFrames +- **Column normalization**: camelCase → snake_case (e.g., `variableId` → + `variable_id`) +- **Data type inference**: Proper types (integers, floats, booleans) +- **Nested data flattening**: Complex structures are normalized into + tabular format + +See [access layer](access_layer.md) for comprehensive documentation on the access layer. + +## API Layer (Low-Level Interface) + +The API layer provides direct access to raw API responses as +dictionaries: + +``` python +from pybdl import BDL + +bdl = BDL() + +# API layer - returns raw dictionaries +levels_data = bdl.api.levels.list_levels() +data_dict = bdl.api.data.get_data_by_variable(variable_id="3643", years=[2021]) + +# Raw API response structure +print(type(levels_data)) # list +print(type(data_dict)) # list or dict +``` + +Use the API layer when you need: + +- Raw API response structure +- Custom response processing +- Direct access to API metadata +- Integration with non-pandas workflows + +See [api clients](api_clients.md) for details about the API layer. + +## Examples + +### Basic Usage with Access Layer + +``` python +from pybdl import BDL, BDLConfig + +# Initialize client +bdl = BDL(BDLConfig(api_key="your-api-key")) + +# Get administrative levels +levels = bdl.levels.list_levels() +print(f"Found {len(levels)} administrative levels") + +# Get variables +variables = bdl.variables.search_variables(name="population") +print(f"Found {len(variables)} population variables") + +# Get data +data = bdl.data.get_data_by_variable("3643", years=[2021], unit_level=2) +print(f"Retrieved {len(data)} data points") +print(data.head()) +``` + +### Using Both Interfaces + +``` python +from pybdl import BDL + +bdl = BDL() + +# Access layer for DataFrame analysis +df = bdl.data.get_data_by_variable("3643", years=[2021]) +avg_value = df['val'].mean() + +# API layer for raw metadata +metadata = bdl.api.data.get_data_by_variable( + "3643", years=[2021], return_metadata=True +) +if isinstance(metadata, tuple): + data, meta = metadata + print(f"Total pages: {meta.get('totalPages', 'unknown')}") +``` + +### Context Manager and Session Lifecycle + +`BDL` can be used as a context manager to ensure HTTP sessions and +underlying resources are properly closed when you are done. This is +especially important in long-running processes and scripts that should +not leave open connections behind. + +``` python +from pybdl import BDL, BDLConfig + +# Context manager — sessions are closed automatically on exit +with BDL(BDLConfig(api_key="your-api-key")) as bdl: + levels = bdl.levels.list_levels() + data = bdl.data.get_data_by_variable("3643", years=[2021]) +``` + +When **not** using the context manager, call `bdl.close()` explicitly +when finished: + +``` python +bdl = BDL() +try: + data = bdl.data.get_data_by_variable("3643", years=[2021]) +finally: + bdl.close() +``` + +The async interface supports `async with`: + +``` python +import asyncio +from pybdl import BDL + +async def main(): + async with BDL() as bdl: + data = await bdl.data.aget_data_by_variable("3643", years=[2021]) + return data + +asyncio.run(main()) +``` + +### Async Usage + +Both interfaces support async operations: + +``` python +import asyncio +from pybdl import BDL + +async def main(): + bdl = BDL() + + # Async access layer + levels_df = await bdl.levels.alist_levels() + variables_df = await bdl.variables.alist_variables() + + # Async API layer + levels_data = await bdl.api.levels.alist_levels() + + return levels_df, variables_df, levels_data + +asyncio.run(main()) +``` + +## API reference + +::: pybdl.client + +!!! seealso + +```markdown +- [Access layer](access_layer.md) — comprehensive access layer documentation +- [API clients](api_clients.md) — API layer details +- [Examples](examples.ipynb) — real-world usage +- [Configuration](config.md) — configuration options +``` diff --git a/docs/main_client.rst b/docs/main_client.rst deleted file mode 100644 index 63c4ec6..0000000 --- a/docs/main_client.rst +++ /dev/null @@ -1,191 +0,0 @@ -Main Client -=========== - -.. automodule:: pybdl.client - :members: - :undoc-members: - :show-inheritance: - :inherited-members: - :noindex: - -The :class:`pybdl.client.BDL` class is the main entry point for the library. It provides **two interfaces** for accessing BDL data: - -1. **Access Layer** (default, recommended): Returns pandas DataFrames - use ``bdl.levels``, ``bdl.data``, etc. -2. **API Layer**: Returns raw dictionaries - use ``bdl.api.levels``, ``bdl.api.data``, etc. - -For most users, the **access layer is recommended** as it provides DataFrames that are immediately ready for data analysis. - -Access Layer (Default Interface) ---------------------------------- - -The access layer is the primary interface and returns pandas DataFrames: - -.. code-block:: python - - from pybdl import BDL, BDLConfig - - bdl = BDL(BDLConfig(api_key="your-api-key")) - - # Access layer - returns DataFrames - levels_df = bdl.levels.list_levels() - variables_df = bdl.variables.list_variables() - data_df = bdl.data.get_data_by_variable(variable_id="3643", years=[2021]) - - # Data is ready for pandas operations - print(levels_df.head()) - print(data_df.dtypes) - print(data_df.columns) - -Key features of the access layer: - -- **Automatic DataFrame conversion**: All responses are pandas DataFrames -- **Column normalization**: camelCase → snake_case (e.g., ``variableId`` → ``variable_id``) -- **Data type inference**: Proper types (integers, floats, booleans) -- **Nested data flattening**: Complex structures are normalized into tabular format - -See :doc:`access_layer` for comprehensive documentation on the access layer. - -API Layer (Low-Level Interface) ---------------------------------- - -The API layer provides direct access to raw API responses as dictionaries: - -.. code-block:: python - - from pybdl import BDL - - bdl = BDL() - - # API layer - returns raw dictionaries - levels_data = bdl.api.levels.list_levels() - data_dict = bdl.api.data.get_data_by_variable(variable_id="3643", years=[2021]) - - # Raw API response structure - print(type(levels_data)) # list - print(type(data_dict)) # list or dict - -Use the API layer when you need: - -- Raw API response structure -- Custom response processing -- Direct access to API metadata -- Integration with non-pandas workflows - -See :doc:`api_clients` for details about the API layer. - -Examples --------- - -Basic Usage with Access Layer -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: python - - from pybdl import BDL, BDLConfig - - # Initialize client - bdl = BDL(BDLConfig(api_key="your-api-key")) - - # Get administrative levels - levels = bdl.levels.list_levels() - print(f"Found {len(levels)} administrative levels") - - # Get variables - variables = bdl.variables.search_variables(name="population") - print(f"Found {len(variables)} population variables") - - # Get data - data = bdl.data.get_data_by_variable("3643", years=[2021], unit_level=2) - print(f"Retrieved {len(data)} data points") - print(data.head()) - -Using Both Interfaces -~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: python - - from pybdl import BDL - - bdl = BDL() - - # Access layer for DataFrame analysis - df = bdl.data.get_data_by_variable("3643", years=[2021]) - avg_value = df['val'].mean() - - # API layer for raw metadata - metadata = bdl.api.data.get_data_by_variable( - "3643", years=[2021], return_metadata=True - ) - if isinstance(metadata, tuple): - data, meta = metadata - print(f"Total pages: {meta.get('totalPages', 'unknown')}") - -Context Manager and Session Lifecycle -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -``BDL`` can be used as a context manager to ensure HTTP sessions and underlying resources are -properly closed when you are done. This is especially important in long-running processes and -scripts that should not leave open connections behind. - -.. code-block:: python - - from pybdl import BDL, BDLConfig - - # Context manager — sessions are closed automatically on exit - with BDL(BDLConfig(api_key="your-api-key")) as bdl: - levels = bdl.levels.list_levels() - data = bdl.data.get_data_by_variable("3643", years=[2021]) - -When **not** using the context manager, call ``bdl.close()`` explicitly when finished: - -.. code-block:: python - - bdl = BDL() - try: - data = bdl.data.get_data_by_variable("3643", years=[2021]) - finally: - bdl.close() - -The async interface supports ``async with``: - -.. code-block:: python - - import asyncio - from pybdl import BDL - - async def main(): - async with BDL() as bdl: - data = await bdl.data.aget_data_by_variable("3643", years=[2021]) - return data - - asyncio.run(main()) - -Async Usage -~~~~~~~~~~~ - -Both interfaces support async operations: - -.. code-block:: python - - import asyncio - from pybdl import BDL - - async def main(): - bdl = BDL() - - # Async access layer - levels_df = await bdl.levels.alist_levels() - variables_df = await bdl.variables.alist_variables() - - # Async API layer - levels_data = await bdl.api.levels.alist_levels() - - return levels_df, variables_df, levels_data - - asyncio.run(main()) - -.. seealso:: - - :doc:`access_layer` for comprehensive access layer documentation - - :doc:`api_clients` for API layer details - - :doc:`examples` for real-world usage examples - - :doc:`config` for configuration options diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 32bb245..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "" goto help - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/docs/rate_limiting.md b/docs/rate_limiting.md new file mode 100644 index 0000000..9f7d750 --- /dev/null +++ b/docs/rate_limiting.md @@ -0,0 +1,381 @@ +# Rate Limiting + +pyBDL includes a sophisticated rate limiting system that automatically +enforces API quotas to prevent exceeding the BDL API provider's limits. +The rate limiter supports both synchronous and asynchronous operations, +persistent quota tracking, and flexible wait/raise behaviors. + +## Overview + +The rate limiting system enforces multiple quota periods simultaneously +(per second, per 15 minutes, per 12 hours, per 7 days) as specified by +the BDL API provider. It automatically tracks quota usage and can either +wait for quota to become available or raise exceptions when limits are +exceeded. + +## Key Features + +- **Automatic enforcement**: Rate limiting is built into all API calls +- **Multiple quota periods**: Enforces limits across different time + windows simultaneously +- **Persistent cache**: Quota usage survives process restarts +- **Sync & async support**: Works seamlessly with both synchronous and + asynchronous code +- **Configurable behavior**: Choose to wait or raise exceptions when + limits are exceeded +- **Shared state**: Sync and async limiters share quota state via + persistent cache + +The `pybdl.config.BDLConfig` used by the API client defaults to +**waiting** when a local quota slot is not yet available +(`raise_on_rate_limit=False`). Set `raise_on_rate_limit=True` (or +environment variable `BDL_RATE_LIMIT_RAISE=true`) to raise +`pybdl.api.exceptions.RateLimitError` immediately instead, for example +in tests that must fail fast. + +When the **server** responds with HTTP **429** (Too Many Requests), the +client retries with a separate budget (`http_429_max_retries` / +`BDL_HTTP_429_MAX_RETRIES`), honoring `Retry-After` when present +(seconds or HTTP-date) up to `http_429_max_delay` (default 900 seconds). +If `Retry-After` is omitted, waits use **exponential backoff** +`retry_backoff_factor × 2^attempt` (capped by `http_429_max_delay`). +This is independent of `request_retries`, which applies to other +retryable status codes. + +## Default Quotas + +The rate limiter enforces the following default quotas based on user +registration status: + +| Period | Anonymous user | Registered user | +|--------|----------------|-----------------| +| 1s | 5 | 10 | +| 15m | 100 | 500 | +| 12h | 1,000 | 5,000 | +| 7d | 10,000 | 50,000 | + +These limits are automatically applied based on whether you provide an +API key (registered user) or not (anonymous user). + +### Registration status detection + +The library automatically determines your registration status: + +- **Anonymous user**: When `api_key` is `None` or not provided in + `BDLConfig` +- **Registered user**: When `api_key` is provided in `BDLConfig` + +The rate limiter uses separate quota tracking for registered and +anonymous users, ensuring that each user type gets the correct limits. + +## User Guide + +### Basic Usage + +Rate limiting is automatically handled by the library. Simply use the +API client normally: + +``` python +from pybdl import BDL, BDLConfig + +config = BDLConfig(api_key="your-api-key") +bdl = BDL(config) + +# Rate limiting is automatic - no extra code needed +data = bdl.api.data.get_data_by_variable(variable_id="3643", years=[2021]) +``` + +The rate limiter will automatically: - Track your API usage across all +calls - Enforce quota limits - Raise exceptions if limits are exceeded +(default behavior) + +### Handling Rate Limit Errors + +By default, the rate limiter raises a `RateLimitError` when quota is +exceeded: + +``` python +from pybdl.utils.rate_limiter import RateLimitError + +try: + data = bdl.api.data.get_data_by_variable(variable_id="3643", years=[2021]) +except RateLimitError as e: + print(f"Rate limit exceeded. Retry after {e.retry_after:.1f} seconds") + print(f"Limit info: {e.limit_info}") +``` + +The exception includes: - `retry_after`: Number of seconds to wait +before retrying - `limit_info`: Dictionary with detailed quota +information + +### Waiting Instead of Raising + +You can configure the rate limiter to wait automatically instead of +raising exceptions. This requires creating a custom rate limiter: + +``` python +from pybdl.utils.rate_limiter import RateLimiter, PersistentQuotaCache +from pybdl.config import DEFAULT_QUOTAS + +# Create a rate limiter that waits up to 30 seconds +cache = PersistentQuotaCache(enabled=True) +quotas = {k: v[1] for k, v in DEFAULT_QUOTAS.items()} # Registered user quotas +limiter = RateLimiter( + quotas=quotas, + is_registered=True, + cache=cache, + raise_on_limit=False, # Wait instead of raising + max_delay=30.0 # Maximum wait time in seconds +) + +# Use the limiter before making API calls +limiter.acquire() +data = bdl.api.data.get_data_by_variable(variable_id="3643", years=[2021]) +``` + +### Using Context Managers + +Rate limiters can be used as context managers for cleaner code: + +``` python +from pybdl.utils.rate_limiter import RateLimiter, PersistentQuotaCache +from pybdl.config import DEFAULT_QUOTAS + +cache = PersistentQuotaCache(enabled=True) +quotas = {k: v[1] for k, v in DEFAULT_QUOTAS.items()} +limiter = RateLimiter(quotas, is_registered=True, cache=cache) + +# Automatically acquires quota when entering context +with limiter: + data = bdl.api.data.get_data_by_variable(variable_id="3643", years=[2021]) +``` + +### Using Decorators + +You can decorate functions to automatically rate limit them: + +``` python +from pybdl.utils.rate_limiter import rate_limit +from pybdl.config import DEFAULT_QUOTAS + +quotas = {k: v[1] for k, v in DEFAULT_QUOTAS.items()} + +@rate_limit(quotas=quotas, is_registered=True, max_delay=10) +def fetch_data(variable_id: str, year: int): + return bdl.api.data.get_data_by_variable(variable_id=variable_id, years=[year]) + +# Function is automatically rate limited +data = fetch_data("3643", 2021) +``` + +For async functions: + +``` python +from pybdl.utils.rate_limiter import async_rate_limit +from pybdl.config import DEFAULT_QUOTAS + +quotas = {k: v[1] for k, v in DEFAULT_QUOTAS.items()} + +@async_rate_limit(quotas=quotas, is_registered=True) +async def async_fetch_data(variable_id: str, year: int): + return await bdl.api.data.aget_data_by_variable(variable_id=variable_id, years=[year]) +``` + +### Checking Remaining Quota + +You can check how much quota remains before making API calls: + +``` python +from pybdl import BDL, BDLConfig + +bdl = BDL(BDLConfig(api_key="your-api-key")) + +# Get remaining quota (requires accessing the internal limiter) +remaining = bdl._client._sync_limiter.get_remaining_quota() +print(f"Remaining requests per second: {remaining.get(1, 0)}") +print(f"Remaining requests per 15 minutes: {remaining.get(900, 0)}") +``` + +### Custom Quotas + +You can override default quotas for testing or special deployments: + +``` python +from pybdl import BDLConfig + +# Custom quotas: period in seconds -> limit +custom_quotas = { + 1: 20, # 20 requests per second + 900: 500, # 500 requests per 15 minutes + 43200: 2000, # 2000 requests per 12 hours + 604800: 20000 # 20000 requests per 7 days +} + +config = BDLConfig(api_key="your-api-key", custom_quotas=custom_quotas) +bdl = BDL(config) +``` + +Or via environment variable: + +``` bash +export BDL_QUOTAS='{"1": 20, "900": 500}' +``` + +### Persistent Cache + +The rate limiter uses a persistent cache to track quota usage across +process restarts. The cache is stored in: + +- **Project-local**: `.cache/pybdl/quota_cache.json` (default) +- **Global**: Platform-specific cache directory (e.g., + `~/.cache/pybdl/quota_cache.json` on Linux) + +You can disable persistent caching: + +``` python +from pybdl import BDLConfig + +config = BDLConfig(api_key="your-api-key", quota_cache_enabled=False) +bdl = BDL(config) +``` + +### Sync and Async Sharing + +Both synchronous and asynchronous rate limiters share the same quota +state via the persistent cache. This means: + +- Sync and async API calls count toward the same limits +- Quota usage persists across different execution contexts +- Process restarts maintain quota state + +## Technical Details + +For technical implementation details, including architecture, algorithm, +thread safety, cache implementation, and configuration options, see +[appendix](appendix.md). + +## API Reference + +::: pybdl.utils.rate_limiter + +## Examples + +### Example: Custom Rate Limiter with Wait Behavior + +``` python +from pybdl.utils.rate_limiter import RateLimiter, PersistentQuotaCache +from pybdl.config import DEFAULT_QUOTAS + +# Create cache +cache = PersistentQuotaCache(enabled=True) + +# Get registered user quotas +quotas = {k: v[1] for k, v in DEFAULT_QUOTAS.items()} + +# Create limiter that waits up to 30 seconds +limiter = RateLimiter( + quotas=quotas, + is_registered=True, + cache=cache, + raise_on_limit=False, + max_delay=30.0 +) + +# Use limiter +limiter.acquire() # Will wait if needed, up to 30 seconds +# Make your API call here +``` + +### Example: Handling Rate Limit Errors + +``` python +from pybdl import BDL, BDLConfig +from pybdl.utils.rate_limiter import RateLimitError, RateLimitDelayExceeded + +bdl = BDL(BDLConfig(api_key="your-api-key")) + +try: + data = bdl.api.data.get_data_by_variable(variable_id="3643", years=[2021]) +except RateLimitError as e: + if isinstance(e, RateLimitDelayExceeded): + print(f"Would need to wait {e.actual_delay:.1f}s, exceeds max {e.max_delay:.1f}s") + else: + print(f"Rate limit exceeded. Retry after {e.retry_after:.1f}s") + print(f"Current limits: {e.limit_info}") +``` + +### Example: Checking Quota Before Making Calls + +``` python +from pybdl import BDL, BDLConfig + +bdl = BDL(BDLConfig(api_key="your-api-key")) + +# Check remaining quota +remaining = bdl._client._sync_limiter.get_remaining_quota() + +if remaining.get(1, 0) < 5: + print("Warning: Low quota remaining for 1-second period") + # Consider waiting or reducing request rate + +# Make API call +data = bdl.api.data.get_data_by_variable(variable_id="3643", years=[2021]) +``` + +### Example: Resetting Quota (for testing) + +``` python +from pybdl import BDL, BDLConfig + +bdl = BDL(BDLConfig(api_key="your-api-key")) + +# Reset quota counters (useful for testing) +bdl._client._sync_limiter.reset() + +# Now you can make fresh API calls +``` + +## Best Practices + +1. **Use default behavior**: The default raise-on-limit behavior is + usually best for most applications +2. **Handle exceptions**: Always catch `RateLimitError` and implement + retry logic +3. **Monitor quota**: Check remaining quota periodically to avoid + hitting limits unexpectedly +4. **Use persistent cache**: Keep `quota_cache_enabled=True` (default) + to maintain quota state across restarts +5. **Custom quotas for testing**: Use custom quotas when testing to + avoid hitting production limits +6. **Async operations**: Use async rate limiters for async code to + avoid blocking the event loop + +## Troubleshooting + +### RateLimitError despite few calls + +The persistent cache may contain old quota data. Try resetting the +quota or clearing the cache file. + +### Sync vs async separate limits + +Ensure both limiters share the same `PersistentQuotaCache` instance. +This is automatic when using `BDLConfig`. + +### Rate limiter feels slow + +Consider using async operations or adjusting `max_delay`. The rate +limiter adds minimal overhead (\<1ms per call). + +### Corrupted cache file + +The cache file is automatically recreated if corrupted. Old quota +data will be lost, but this is usually fine. + +!!! seealso + +```markdown +- [Configuration](config.md) — configuration options +- [API clients](api_clients.md) — API usage examples +- [Appendix](appendix.md) — technical implementation details +``` diff --git a/docs/rate_limiting.rst b/docs/rate_limiting.rst deleted file mode 100644 index a2a8170..0000000 --- a/docs/rate_limiting.rst +++ /dev/null @@ -1,365 +0,0 @@ -Rate Limiting -============== - -pyBDL includes a sophisticated rate limiting system that automatically enforces API quotas to prevent exceeding the BDL API provider's limits. The rate limiter supports both synchronous and asynchronous operations, persistent quota tracking, and flexible wait/raise behaviors. - -Overview --------- - -The rate limiting system enforces multiple quota periods simultaneously (per second, per 15 minutes, per 12 hours, per 7 days) as specified by the BDL API provider. It automatically tracks quota usage and can either wait for quota to become available or raise exceptions when limits are exceeded. - -Key Features ------------- - -- **Automatic enforcement**: Rate limiting is built into all API calls -- **Multiple quota periods**: Enforces limits across different time windows simultaneously -- **Persistent cache**: Quota usage survives process restarts -- **Sync & async support**: Works seamlessly with both synchronous and asynchronous code -- **Configurable behavior**: Choose to wait or raise exceptions when limits are exceeded -- **Shared state**: Sync and async limiters share quota state via persistent cache - -The :class:`pybdl.config.BDLConfig` used by the API client defaults to **waiting** when a local quota slot is not yet available (``raise_on_rate_limit=False``). Set ``raise_on_rate_limit=True`` (or environment variable ``BDL_RATE_LIMIT_RAISE=true``) to raise :class:`pybdl.api.exceptions.RateLimitError` immediately instead, for example in tests that must fail fast. - -When the **server** responds with HTTP **429** (Too Many Requests), the client retries with a separate budget (``http_429_max_retries`` / ``BDL_HTTP_429_MAX_RETRIES``), honoring ``Retry-After`` when present (seconds or HTTP-date) up to ``http_429_max_delay`` (default 900 seconds). If ``Retry-After`` is omitted, waits use **exponential backoff** ``retry_backoff_factor × 2^attempt`` (capped by ``http_429_max_delay``). This is independent of ``request_retries``, which applies to other retryable status codes. - -Default Quotas --------------- - -The rate limiter enforces the following default quotas based on user registration status: - -+---------+------------------+-------------------+ -| Period | Anonymous user | Registered user | -+=========+==================+===================+ -| 1s | 5 | 10 | -+---------+------------------+-------------------+ -| 15m | 100 | 500 | -+---------+------------------+-------------------+ -| 12h | 1,000 | 5,000 | -+---------+------------------+-------------------+ -| 7d | 10,000 | 50,000 | -+---------+------------------+-------------------+ - -These limits are automatically applied based on whether you provide an API key (registered user) or not (anonymous user). - -**Registration Status Detection** - -The library automatically determines your registration status: - -- **Anonymous user**: When ``api_key`` is ``None`` or not provided in ``BDLConfig`` -- **Registered user**: When ``api_key`` is provided in ``BDLConfig`` - -The rate limiter uses separate quota tracking for registered and anonymous users, ensuring that each user type gets the correct limits. - -User Guide ----------- - -Basic Usage -~~~~~~~~~~~ - -Rate limiting is automatically handled by the library. Simply use the API client normally: - -.. code-block:: python - - from pybdl import BDL, BDLConfig - - config = BDLConfig(api_key="your-api-key") - bdl = BDL(config) - - # Rate limiting is automatic - no extra code needed - data = bdl.api.data.get_data_by_variable(variable_id="3643", years=[2021]) - -The rate limiter will automatically: -- Track your API usage across all calls -- Enforce quota limits -- Raise exceptions if limits are exceeded (default behavior) - -Handling Rate Limit Errors -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -By default, the rate limiter raises a :class:`RateLimitError` when quota is exceeded: - -.. code-block:: python - - from pybdl.utils.rate_limiter import RateLimitError - - try: - data = bdl.api.data.get_data_by_variable(variable_id="3643", years=[2021]) - except RateLimitError as e: - print(f"Rate limit exceeded. Retry after {e.retry_after:.1f} seconds") - print(f"Limit info: {e.limit_info}") - -The exception includes: -- ``retry_after``: Number of seconds to wait before retrying -- ``limit_info``: Dictionary with detailed quota information - -Waiting Instead of Raising -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can configure the rate limiter to wait automatically instead of raising exceptions. This requires creating a custom rate limiter: - -.. code-block:: python - - from pybdl.utils.rate_limiter import RateLimiter, PersistentQuotaCache - from pybdl.config import DEFAULT_QUOTAS - - # Create a rate limiter that waits up to 30 seconds - cache = PersistentQuotaCache(enabled=True) - quotas = {k: v[1] for k, v in DEFAULT_QUOTAS.items()} # Registered user quotas - limiter = RateLimiter( - quotas=quotas, - is_registered=True, - cache=cache, - raise_on_limit=False, # Wait instead of raising - max_delay=30.0 # Maximum wait time in seconds - ) - - # Use the limiter before making API calls - limiter.acquire() - data = bdl.api.data.get_data_by_variable(variable_id="3643", years=[2021]) - -Using Context Managers -~~~~~~~~~~~~~~~~~~~~~~ - -Rate limiters can be used as context managers for cleaner code: - -.. code-block:: python - - from pybdl.utils.rate_limiter import RateLimiter, PersistentQuotaCache - from pybdl.config import DEFAULT_QUOTAS - - cache = PersistentQuotaCache(enabled=True) - quotas = {k: v[1] for k, v in DEFAULT_QUOTAS.items()} - limiter = RateLimiter(quotas, is_registered=True, cache=cache) - - # Automatically acquires quota when entering context - with limiter: - data = bdl.api.data.get_data_by_variable(variable_id="3643", years=[2021]) - -Using Decorators -~~~~~~~~~~~~~~~~ - -You can decorate functions to automatically rate limit them: - -.. code-block:: python - - from pybdl.utils.rate_limiter import rate_limit - from pybdl.config import DEFAULT_QUOTAS - - quotas = {k: v[1] for k, v in DEFAULT_QUOTAS.items()} - - @rate_limit(quotas=quotas, is_registered=True, max_delay=10) - def fetch_data(variable_id: str, year: int): - return bdl.api.data.get_data_by_variable(variable_id=variable_id, years=[year]) - - # Function is automatically rate limited - data = fetch_data("3643", 2021) - -For async functions: - -.. code-block:: python - - from pybdl.utils.rate_limiter import async_rate_limit - from pybdl.config import DEFAULT_QUOTAS - - quotas = {k: v[1] for k, v in DEFAULT_QUOTAS.items()} - - @async_rate_limit(quotas=quotas, is_registered=True) - async def async_fetch_data(variable_id: str, year: int): - return await bdl.api.data.aget_data_by_variable(variable_id=variable_id, years=[year]) - -Checking Remaining Quota -~~~~~~~~~~~~~~~~~~~~~~~~ - -You can check how much quota remains before making API calls: - -.. code-block:: python - - from pybdl import BDL, BDLConfig - - bdl = BDL(BDLConfig(api_key="your-api-key")) - - # Get remaining quota (requires accessing the internal limiter) - remaining = bdl._client._sync_limiter.get_remaining_quota() - print(f"Remaining requests per second: {remaining.get(1, 0)}") - print(f"Remaining requests per 15 minutes: {remaining.get(900, 0)}") - -Custom Quotas -~~~~~~~~~~~~~ - -You can override default quotas for testing or special deployments: - -.. code-block:: python - - from pybdl import BDLConfig - - # Custom quotas: period in seconds -> limit - custom_quotas = { - 1: 20, # 20 requests per second - 900: 500, # 500 requests per 15 minutes - 43200: 2000, # 2000 requests per 12 hours - 604800: 20000 # 20000 requests per 7 days - } - - config = BDLConfig(api_key="your-api-key", custom_quotas=custom_quotas) - bdl = BDL(config) - -Or via environment variable: - -.. code-block:: bash - - export BDL_QUOTAS='{"1": 20, "900": 500}' - -Persistent Cache -~~~~~~~~~~~~~~~~ - -The rate limiter uses a persistent cache to track quota usage across process restarts. The cache is stored in: - -- **Project-local**: ``.cache/pybdl/quota_cache.json`` (default) -- **Global**: Platform-specific cache directory (e.g., ``~/.cache/pybdl/quota_cache.json`` on Linux) - -You can disable persistent caching: - -.. code-block:: python - - from pybdl import BDLConfig - - config = BDLConfig(api_key="your-api-key", quota_cache_enabled=False) - bdl = BDL(config) - -Sync and Async Sharing -~~~~~~~~~~~~~~~~~~~~~~ - -Both synchronous and asynchronous rate limiters share the same quota state via the persistent cache. This means: - -- Sync and async API calls count toward the same limits -- Quota usage persists across different execution contexts -- Process restarts maintain quota state - -Technical Details ------------------ - -For technical implementation details, including architecture, algorithm, thread safety, cache implementation, and configuration options, see :doc:`appendix`. - -API Reference -------------- - -.. automodule:: pybdl.utils.rate_limiter - :members: - :undoc-members: - :show-inheritance: - -Examples --------- - -Example: Custom Rate Limiter with Wait Behavior -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: python - - from pybdl.utils.rate_limiter import RateLimiter, PersistentQuotaCache - from pybdl.config import DEFAULT_QUOTAS - - # Create cache - cache = PersistentQuotaCache(enabled=True) - - # Get registered user quotas - quotas = {k: v[1] for k, v in DEFAULT_QUOTAS.items()} - - # Create limiter that waits up to 30 seconds - limiter = RateLimiter( - quotas=quotas, - is_registered=True, - cache=cache, - raise_on_limit=False, - max_delay=30.0 - ) - - # Use limiter - limiter.acquire() # Will wait if needed, up to 30 seconds - # Make your API call here - -Example: Handling Rate Limit Errors -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: python - - from pybdl import BDL, BDLConfig - from pybdl.utils.rate_limiter import RateLimitError, RateLimitDelayExceeded - - bdl = BDL(BDLConfig(api_key="your-api-key")) - - try: - data = bdl.api.data.get_data_by_variable(variable_id="3643", years=[2021]) - except RateLimitError as e: - if isinstance(e, RateLimitDelayExceeded): - print(f"Would need to wait {e.actual_delay:.1f}s, exceeds max {e.max_delay:.1f}s") - else: - print(f"Rate limit exceeded. Retry after {e.retry_after:.1f}s") - print(f"Current limits: {e.limit_info}") - -Example: Checking Quota Before Making Calls -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: python - - from pybdl import BDL, BDLConfig - - bdl = BDL(BDLConfig(api_key="your-api-key")) - - # Check remaining quota - remaining = bdl._client._sync_limiter.get_remaining_quota() - - if remaining.get(1, 0) < 5: - print("Warning: Low quota remaining for 1-second period") - # Consider waiting or reducing request rate - - # Make API call - data = bdl.api.data.get_data_by_variable(variable_id="3643", years=[2021]) - -Example: Resetting Quota (for testing) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: python - - from pybdl import BDL, BDLConfig - - bdl = BDL(BDLConfig(api_key="your-api-key")) - - # Reset quota counters (useful for testing) - bdl._client._sync_limiter.reset() - - # Now you can make fresh API calls - -Best Practices --------------- - -1. **Use default behavior**: The default raise-on-limit behavior is usually best for most applications -2. **Handle exceptions**: Always catch ``RateLimitError`` and implement retry logic -3. **Monitor quota**: Check remaining quota periodically to avoid hitting limits unexpectedly -4. **Use persistent cache**: Keep ``quota_cache_enabled=True`` (default) to maintain quota state across restarts -5. **Custom quotas for testing**: Use custom quotas when testing to avoid hitting production limits -6. **Async operations**: Use async rate limiters for async code to avoid blocking the event loop - -Troubleshooting ---------------- - -**Q: I'm getting RateLimitError even though I haven't made many calls** - -A: The persistent cache may contain old quota data. Try resetting the quota or clearing the cache file. - -**Q: Sync and async calls seem to have separate limits** - -A: Ensure both limiters share the same ``PersistentQuotaCache`` instance. This is automatic when using ``BDLConfig``. - -**Q: Rate limiter is too slow** - -A: Consider using async operations or adjusting ``max_delay``. The rate limiter adds minimal overhead (<1ms per call). - -**Q: Cache file is corrupted** - -A: The cache file is automatically recreated if corrupted. Old quota data will be lost, but this is usually fine. - -.. seealso:: - - :doc:`config` for configuration options - - :doc:`api_clients` for API usage examples - - :doc:`appendix` for technical implementation details diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..998399a --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,93 @@ +--- +site_name: pyBDL +site_description: Python client for Polish GUS Local Data Bank (BDL) +site_url: https://an0da.github.io/pybdl/ +repo_url: https://github.com/AN0DA/pybdl +repo_name: AN0DA/pybdl +edit_uri: edit/main/docs/ + +docs_dir: docs +site_dir: site +strict: true + +exclude_docs: | + conf.py + _build/** + .cache/** + +watch: + - pybdl + +theme: + name: material + features: + - content.code.copy + - navigation.tabs + - navigation.sections + - search.suggest + - search.highlight + - navigation.expand + - toc.follow + palette: + - scheme: default + primary: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - scheme: slate + primary: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + +extra: + version: + provider: mike + +plugins: + - search + - include-markdown + - mkdocs-jupyter: + execute: false + - mkdocstrings: + handlers: + python: + paths: [.] + options: + docstring_style: google + members_order: source + show_root_heading: true + show_root_full_path: false + show_source: true + show_if_no_docstring: true + inherited_members: true + merge_init_into_class: true + show_bases: true + separate_signature: true + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - admonition + - pymdownx.details + - tables + - toc: + permalink: true + +nav: + - Home: index.md + - User Guide: + - Main client: main_client.md + - Access layer: access_layer.md + - Examples: examples.ipynb + - API clients: api_clients.md + - Configuration: config.md + - Rate limiting: rate_limiting.md + - Reference: + - Appendix: appendix.md + - Changelog: changelog.md diff --git a/pybdl/utils/__init__.py b/pybdl/utils/__init__.py new file mode 100644 index 0000000..9e436e4 --- /dev/null +++ b/pybdl/utils/__init__.py @@ -0,0 +1 @@ +"""Internal utilities for pyBDL.""" diff --git a/pyproject.toml b/pyproject.toml index f0d0960..8e65eca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,10 +37,11 @@ dev = [ "ruff>=0.9.10", ] docs = [ - "sphinx>=8.2.3", - "sphinx-autodoc-typehints>=3.2.0", - "sphinx-rtd-theme>=3.0.2", - "myst-nb>=1.1.0", + "mike>=2.1.0", + "mkdocs-include-markdown-plugin>=7.2.0", + "mkdocs-jupyter>=0.25.1", + "mkdocs-material>=9.6.0", + "mkdocstrings[python]>=0.30.0", ] [tool.ruff] diff --git a/uv.lock b/uv.lock index b2782b0..45f2651 100644 --- a/uv.lock +++ b/uv.lock @@ -13,15 +13,6 @@ resolution-markers = [ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] -[[package]] -name = "alabaster" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, -] - [[package]] name = "anyio" version = "4.13.0" @@ -148,6 +139,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] +[[package]] +name = "backrefs" +version = "6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/a6/e325ec73b638d3ede4421b5445d4a0b8b219481826cc079d510100af356c/backrefs-6.2.tar.gz", hash = "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49", size = 7012303, upload-time = "2026-02-16T19:10:15.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/39/3765df263e08a4df37f4f43cb5aa3c6c17a4bdd42ecfe841e04c26037171/backrefs-6.2-py310-none-any.whl", hash = "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8", size = 381075, upload-time = "2026-02-16T19:10:04.322Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl", hash = "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be", size = 392874, upload-time = "2026-02-16T19:10:06.314Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/77e8c9745b4d227cce9f5e0a6f68041278c5f9b18588b35905f5f19c1beb/backrefs-6.2-py312-none-any.whl", hash = "sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90", size = 398787, upload-time = "2026-02-16T19:10:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl", hash = "sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b", size = 400747, upload-time = "2026-02-16T19:10:09.791Z" }, + { url = "https://files.pythonhosted.org/packages/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-none-any.whl", hash = "sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7", size = 412602, upload-time = "2026-02-16T19:10:12.317Z" }, + { url = "https://files.pythonhosted.org/packages/21/f8/d02f650c47d05034dcd6f9c8cf94f39598b7a89c00ecda0ecb2911bc27e9/backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7", size = 381077, upload-time = "2026-02-16T19:10:13.74Z" }, +] + [[package]] name = "bandit" version = "1.9.4" @@ -193,6 +198,15 @@ css = [ { name = "tinycss2" }, ] +[[package]] +name = "bracex" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, +] + [[package]] name = "certifi" version = "2026.2.25" @@ -629,15 +643,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] -[[package]] -name = "docutils" -version = "0.22.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, -] - [[package]] name = "execnet" version = "2.1.2" @@ -724,50 +729,24 @@ wheels = [ ] [[package]] -name = "greenlet" -version = "3.3.2" +name = "ghp-import" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" }, - { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" }, - { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" }, - { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" }, - { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" }, - { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5", size = 230389, upload-time = "2026-02-20T20:17:18.772Z" }, - { url = "https://files.pythonhosted.org/packages/e1/a1/65bbc059a43a7e2143ec4fc1f9e3f673e04f9c7b371a494a101422ac4fd5/greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd", size = 229645, upload-time = "2026-02-20T20:18:18.695Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, - { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, - { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, - { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, - { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, - { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, - { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, - { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, - { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, - { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, - { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, - { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, - { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, - { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, - { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, - { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, - { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, - { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, - { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, - { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, - { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, - { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, - { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, - { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, +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 = "griffelib" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, ] [[package]] @@ -835,27 +814,6 @@ 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 = "imagesize" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, -] - -[[package]] -name = "importlib-metadata" -version = "9.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -1052,25 +1010,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] -[[package]] -name = "jupyter-cache" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "click" }, - { name = "importlib-metadata" }, - { name = "nbclient" }, - { name = "nbformat" }, - { name = "pyyaml" }, - { name = "sqlalchemy" }, - { name = "tabulate" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bb/f7/3627358075f183956e8c4974603232b03afd4ddc7baf72c2bc9fff522291/jupyter_cache-1.0.1.tar.gz", hash = "sha256:16e808eb19e3fb67a223db906e131ea6e01f03aa27f49a7214ce6a5fec186fb9", size = 32048, upload-time = "2024-11-15T16:03:55.322Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/6b/67b87da9d36bff9df7d0efbd1a325fa372a43be7158effaf43ed7b22341d/jupyter_cache-1.0.1-py3-none-any.whl", hash = "sha256:9c3cafd825ba7da8b5830485343091143dff903e4d8c69db9349b728b140abf6", size = 33907, upload-time = "2024-11-15T16:03:54.021Z" }, -] - [[package]] name = "jupyter-client" version = "8.8.0" @@ -1225,6 +1164,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl", hash = "sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968", size = 59830, upload-time = "2025-10-22T13:59:16.767Z" }, ] +[[package]] +name = "jupytext" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/a5/80c02f307c8ce863cb33e27daf049315e9d96979e14eead700923b5ec9cc/jupytext-1.19.1.tar.gz", hash = "sha256:82587c07e299173c70ed5e8ec7e75183edf1be289ed518bab49ad0d4e3d5f433", size = 4307829, upload-time = "2026-01-25T21:35:13.276Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl", hash = "sha256:d8975035155d034bdfde5c0c37891425314b7ea8d3a6c4b5d18c294348714cd9", size = 170478, upload-time = "2026-01-25T21:35:11.17Z" }, +] + [[package]] name = "kiwisolver" version = "1.5.0" @@ -1413,6 +1368,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, ] +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -1596,6 +1560,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "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 = "mike" +version = "2.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "mkdocs" }, + { name = "pyparsing" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "verspec" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/09/de1cab0018eb5f1fbd9dcc26b6e61f9453c5ec2eb790949d6ed75e1ffe55/mike-2.1.4.tar.gz", hash = "sha256:75d549420b134603805a65fc67f7dcd9fcd0ad1454fb2c893d9e844cba1aa6e4", size = 38190, upload-time = "2026-03-08T02:46:29.187Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/f7/10f5e101db25741b91e4f4792c5d97b4fa834ead5cf509ae91097d939424/mike-2.1.4-py3-none-any.whl", hash = "sha256:39933e992e155dd70f2297e749a0ed78d8fd7942bc33a3666195d177758a280e", size = 33820, upload-time = "2026-03-08T02:46:28.149Z" }, +] + [[package]] name = "mistune" version = "3.2.0" @@ -1605,6 +1595,155 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" }, ] +[[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.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocs-include-markdown-plugin" +version = "7.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, + { name = "wcmatch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/03/cd5e4383e677a3192127c4da67cb6046a8b1ae32ef6201f4faffd4b0c7a5/mkdocs_include_markdown_plugin-7.2.1.tar.gz", hash = "sha256:5d94db87b06cd303619dbaebba5f7f43a3ded7fd7709451d26f08c176376ffec", size = 25395, upload-time = "2026-01-25T15:02:27.861Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/0f/73a1d330183e79b21ee1b1a5dd4102fad1bd70231cf3b0620a7391b3c813/mkdocs_include_markdown_plugin-7.2.1-py3-none-any.whl", hash = "sha256:30da634c568ea5d5f9e5881d51f80ac30d8c5f891cec160344ad7a0fdaea6286", size = 29512, upload-time = "2026-01-25T15:02:26.333Z" }, +] + +[[package]] +name = "mkdocs-jupyter" +version = "0.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "jupytext" }, + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "nbconvert" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/d8/c146ea8cc36c3e812dd4c154513aa308614f35d2b4becec4b449165088f5/mkdocs_jupyter-0.26.1.tar.gz", hash = "sha256:7c80c0d3953de91e5b40a0d3209233795c8f800243ab298e4ec38e0504eda630", size = 1628270, upload-time = "2026-03-24T15:32:47.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/89/eb601278b12c471235860992f5973cf3c55ca3f77d1d6127389eb045a021/mkdocs_jupyter-0.26.1-py3-none-any.whl", hash = "sha256:527242c2c8f1d30970764bbab752de41243e5703f458d8bc05336ec53828192e", size = 1459618, upload-time = "2026-03-24T15:32:46.25Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.6" +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/45/29/6d2bcf41ae40802c4beda2432396fff97b8456fb496371d1bc7aad6512ec/mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69", size = 4097959, upload-time = "2026-03-19T15:41:58.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba", size = 9305470, upload-time = "2026-03-19T15:41:55.217Z" }, +] + +[[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 = "1.0.3" +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/46/62/0dfc5719514115bf1781f44b1d7f2a0923fcc01e9c5d7990e48a05c9ae5d/mkdocstrings-1.0.3.tar.gz", hash = "sha256:ab670f55040722b49bb45865b2e93b824450fb4aef638b00d7acb493a9020434", size = 100946, upload-time = "2026-02-07T14:31:40.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-py3-none-any.whl", hash = "sha256:0d66d18430c2201dc7fe85134277382baaa15e6b30979f3f3bdbabd6dbdb6046", size = 35523, upload-time = "2026-02-07T14:31:39.27Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" }, +] + [[package]] name = "msgpack" version = "1.1.2" @@ -1706,47 +1845,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] -[[package]] -name = "myst-nb" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "importlib-metadata" }, - { name = "ipykernel" }, - { name = "ipython", version = "9.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "ipython", version = "9.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "jupyter-cache" }, - { name = "myst-parser" }, - { name = "nbclient" }, - { name = "nbformat" }, - { name = "pyyaml" }, - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bd/b4/ff1abeea67e8cfe0a8c033389f6d1d8b0bfecfd611befb5cbdeab884fce6/myst_nb-1.4.0.tar.gz", hash = "sha256:c145598de62446a6fd009773dd071a40d3b76106ace780de1abdfc6961f614c2", size = 82285, upload-time = "2026-03-02T21:14:56.95Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/93/0a378b48488879a1d925b42a804edfc6e0cd0ef854220f2dce738a46e7e9/myst_nb-1.4.0-py3-none-any.whl", hash = "sha256:0e2c86e7d3b82c3aa51383f82d6268f7714f3b772c23a796ab09538a8e68b4e4", size = 82555, upload-time = "2026-03-02T21:14:55.652Z" }, -] - -[[package]] -name = "myst-parser" -version = "5.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docutils" }, - { name = "jinja2" }, - { name = "markdown-it-py" }, - { name = "mdit-py-plugins" }, - { name = "pyyaml" }, - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/33/fa/7b45eef11b7971f0beb29d27b7bfe0d747d063aa29e170d9edd004733c8a/myst_parser-5.0.0.tar.gz", hash = "sha256:f6f231452c56e8baa662cc352c548158f6a16fcbd6e3800fc594978002b94f3a", size = 98535, upload-time = "2026-01-15T09:08:18.036Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/ac/686789b9145413f1a61878c407210e41bfdb097976864e0913078b24098c/myst_parser-5.0.0-py3-none-any.whl", hash = "sha256:ab31e516024918296e169139072b81592336f2fef55b8986aa31c9f04b5f7211", size = 84533, upload-time = "2026-01-15T09:08:16.788Z" }, -] - [[package]] name = "nbclient" version = "0.10.4" @@ -1936,6 +2034,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] +[[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 = "3.0.1" @@ -2233,7 +2340,7 @@ wheels = [ [[package]] name = "pybdl" -version = "0.1.0" +version = "1.0.0" source = { editable = "." } dependencies = [ { name = "hishel", extra = ["async"] }, @@ -2265,12 +2372,11 @@ dev = [ { name = "ruff" }, ] docs = [ - { name = "myst-nb" }, - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "sphinx-autodoc-typehints", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "sphinx-autodoc-typehints", version = "3.9.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "sphinx-rtd-theme" }, + { name = "mike" }, + { name = "mkdocs-include-markdown-plugin" }, + { name = "mkdocs-jupyter" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extra = ["python"] }, ] [package.metadata] @@ -2301,10 +2407,11 @@ dev = [ { name = "ruff", specifier = ">=0.9.10" }, ] docs = [ - { name = "myst-nb", specifier = ">=1.1.0" }, - { name = "sphinx", specifier = ">=8.2.3" }, - { name = "sphinx-autodoc-typehints", specifier = ">=3.2.0" }, - { name = "sphinx-rtd-theme", specifier = ">=3.0.2" }, + { name = "mike", specifier = ">=2.1.0" }, + { name = "mkdocs-include-markdown-plugin", specifier = ">=7.2.0" }, + { name = "mkdocs-jupyter", specifier = ">=0.25.1" }, + { name = "mkdocs-material", specifier = ">=9.6.0" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.0" }, ] [[package]] @@ -2325,6 +2432,19 @@ 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.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/63/06673d1eb6d8f83c0ea1f677d770e12565fb516928b4109c9e2055656a9e/pymdown_extensions-10.21.tar.gz", hash = "sha256:39f4a020f40773f6b2ff31d2cd2546c2c04d0a6498c31d9c688d2be07e1767d5", size = 853363, upload-time = "2026-02-15T20:44:06.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl", hash = "sha256:91b879f9f864d49794c2d9534372b10150e6141096c3908a455e45ca72ad9d3f", size = 268877, upload-time = "2026-02-15T20:44:05.464Z" }, +] + [[package]] name = "pyparsing" version = "3.3.2" @@ -2498,6 +2618,18 @@ wheels = [ { 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 = "pyzmq" version = "27.1.0" @@ -2643,15 +2775,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, ] -[[package]] -name = "roman-numerals" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, -] - [[package]] name = "rpds-py" version = "0.30.0" @@ -2826,15 +2949,6 @@ 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 = "snowballstemmer" -version = "3.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, -] - [[package]] name = "soupsieve" version = "2.8.3" @@ -2844,247 +2958,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, ] -[[package]] -name = "sphinx" -version = "9.0.4" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] -dependencies = [ - { name = "alabaster", marker = "python_full_version < '3.12'" }, - { name = "babel", marker = "python_full_version < '3.12'" }, - { name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version < '3.12'" }, - { name = "imagesize", marker = "python_full_version < '3.12'" }, - { name = "jinja2", marker = "python_full_version < '3.12'" }, - { name = "packaging", marker = "python_full_version < '3.12'" }, - { name = "pygments", marker = "python_full_version < '3.12'" }, - { name = "requests", marker = "python_full_version < '3.12'" }, - { name = "roman-numerals", marker = "python_full_version < '3.12'" }, - { name = "snowballstemmer", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.12'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, -] - -[[package]] -name = "sphinx" -version = "9.1.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] -dependencies = [ - { name = "alabaster", marker = "python_full_version >= '3.12'" }, - { name = "babel", marker = "python_full_version >= '3.12'" }, - { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version >= '3.12'" }, - { name = "imagesize", marker = "python_full_version >= '3.12'" }, - { name = "jinja2", marker = "python_full_version >= '3.12'" }, - { name = "packaging", marker = "python_full_version >= '3.12'" }, - { name = "pygments", marker = "python_full_version >= '3.12'" }, - { name = "requests", marker = "python_full_version >= '3.12'" }, - { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, - { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, -] - -[[package]] -name = "sphinx-autodoc-typehints" -version = "3.6.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] -dependencies = [ - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1d/f6/bdd93582b2aaad2cfe9eb5695a44883c8bc44572dd3c351a947acbb13789/sphinx_autodoc_typehints-3.6.1.tar.gz", hash = "sha256:fa0b686ae1b85965116c88260e5e4b82faec3687c2e94d6a10f9b36c3743e2fe", size = 37563, upload-time = "2026-01-02T15:23:46.543Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/6a/c0360b115c81d449b3b73bf74b64ca773464d5c7b1b77bda87c5e874853b/sphinx_autodoc_typehints-3.6.1-py3-none-any.whl", hash = "sha256:dd818ba31d4c97f219a8c0fcacef280424f84a3589cedcb73003ad99c7da41ca", size = 20869, upload-time = "2026-01-02T15:23:45.194Z" }, -] - -[[package]] -name = "sphinx-autodoc-typehints" -version = "3.9.11" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] -dependencies = [ - { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/12/e9/d29ae58dd12971d2cbb872884676a70d1a5e4719b4d82e197264cdf0431a/sphinx_autodoc_typehints-3.9.11.tar.gz", hash = "sha256:28516c916b41fa83271ee2ab9191b73807e4113d3bfb94222ac87d8d9795b6e7", size = 70261, upload-time = "2026-03-24T16:57:28.462Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/e3/ff212b51c16717681792eaf18691e6b5affbbb3d4290147c457fa9127372/sphinx_autodoc_typehints-3.9.11-py3-none-any.whl", hash = "sha256:b5cbc7a56a9338021ab7a4e6aa132aa7829fa2f8b64eca927faab64cd3971b80", size = 37279, upload-time = "2026-03-24T16:57:27.147Z" }, -] - -[[package]] -name = "sphinx-rtd-theme" -version = "3.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docutils" }, - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-jquery" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/84/68/a1bfbf38c0f7bccc9b10bbf76b94606f64acb1552ae394f0b8285bfaea25/sphinx_rtd_theme-3.1.0.tar.gz", hash = "sha256:b44276f2c276e909239a4f6c955aa667aaafeb78597923b1c60babc76db78e4c", size = 7620915, upload-time = "2026-01-12T16:03:31.17Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/c7/b5c8015d823bfda1a346adb2c634a2101d50bb75d421eb6dcb31acd25ebc/sphinx_rtd_theme-3.1.0-py2.py3-none-any.whl", hash = "sha256:1785824ae8e6632060490f67cf3a72d404a85d2d9fc26bce3619944de5682b89", size = 7655617, upload-time = "2026-01-12T16:03:28.101Z" }, -] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, -] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, -] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, -] - -[[package]] -name = "sphinxcontrib-jquery" -version = "4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104, upload-time = "2023-03-14T15:01:00.356Z" }, -] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, -] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, -] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, -] - -[[package]] -name = "sqlalchemy" -version = "2.0.48" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/6d/b8b78b5b80f3c3ab3f7fa90faa195ec3401f6d884b60221260fd4d51864c/sqlalchemy-2.0.48-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b4c575df7368b3b13e0cebf01d4679f9a28ed2ae6c1cd0b1d5beffb6b2007dc", size = 2157184, upload-time = "2026-03-02T15:38:28.161Z" }, - { url = "https://files.pythonhosted.org/packages/21/4b/4f3d4a43743ab58b95b9ddf5580a265b593d017693df9e08bd55780af5bb/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e83e3f959aaa1c9df95c22c528096d94848a1bc819f5d0ebf7ee3df0ca63db6c", size = 3313555, upload-time = "2026-03-02T15:58:57.21Z" }, - { url = "https://files.pythonhosted.org/packages/21/dd/3b7c53f1dbbf736fd27041aee68f8ac52226b610f914085b1652c2323442/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f7b7243850edd0b8b97043f04748f31de50cf426e939def5c16bedb540698f7", size = 3313057, upload-time = "2026-03-02T15:52:29.366Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cc/3e600a90ae64047f33313d7d32e5ad025417f09d2ded487e8284b5e21a15/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82745b03b4043e04600a6b665cb98697c4339b24e34d74b0a2ac0a2488b6f94d", size = 3265431, upload-time = "2026-03-02T15:58:59.096Z" }, - { url = "https://files.pythonhosted.org/packages/8b/19/780138dacfe3f5024f4cf96e4005e91edf6653d53d3673be4844578faf1d/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5e088bf43f6ee6fec7dbf1ef7ff7774a616c236b5c0cb3e00662dd71a56b571", size = 3287646, upload-time = "2026-03-02T15:52:31.569Z" }, - { url = "https://files.pythonhosted.org/packages/40/fd/f32ced124f01a23151f4777e4c705f3a470adc7bd241d9f36a7c941a33bf/sqlalchemy-2.0.48-cp311-cp311-win32.whl", hash = "sha256:9c7d0a77e36b5f4b01ca398482230ab792061d243d715299b44a0b55c89fe617", size = 2116956, upload-time = "2026-03-02T15:46:54.535Z" }, - { url = "https://files.pythonhosted.org/packages/58/d5/dd767277f6feef12d05651538f280277e661698f617fa4d086cce6055416/sqlalchemy-2.0.48-cp311-cp311-win_amd64.whl", hash = "sha256:583849c743e0e3c9bb7446f5b5addeacedc168d657a69b418063dfdb2d90081c", size = 2141627, upload-time = "2026-03-02T15:46:55.849Z" }, - { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737, upload-time = "2026-03-02T15:49:13.207Z" }, - { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020, upload-time = "2026-03-02T15:50:34.547Z" }, - { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983, upload-time = "2026-03-02T15:53:25.542Z" }, - { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690, upload-time = "2026-03-02T15:50:36.277Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738, upload-time = "2026-03-02T15:53:27.519Z" }, - { url = "https://files.pythonhosted.org/packages/4b/44/1d257d9f9556661e7bdc83667cc414ba210acfc110c82938cb3611eea58f/sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99", size = 2115546, upload-time = "2026-03-02T15:54:31.591Z" }, - { url = "https://files.pythonhosted.org/packages/f2/af/c3c7e1f3a2b383155a16454df62ae8c62a30dd238e42e68c24cebebbfae6/sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a", size = 2142484, upload-time = "2026-03-02T15:54:34.072Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599, upload-time = "2026-03-02T15:49:14.41Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825, upload-time = "2026-03-02T15:50:38.269Z" }, - { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200, upload-time = "2026-03-02T15:53:29.366Z" }, - { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876, upload-time = "2026-03-02T15:50:39.802Z" }, - { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045, upload-time = "2026-03-02T15:53:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/6f/82/ea4665d1bb98c50c19666e672f21b81356bd6077c4574e3d2bbb84541f53/sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131", size = 2113700, upload-time = "2026-03-02T15:54:35.825Z" }, - { url = "https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2", size = 2139487, upload-time = "2026-03-02T15:54:37.079Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851, upload-time = "2026-03-02T15:57:48.607Z" }, - { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525, upload-time = "2026-03-02T16:04:38.058Z" }, - { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611, upload-time = "2026-03-02T15:57:50.759Z" }, - { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812, upload-time = "2026-03-02T16:04:40.092Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ae/29b87775fadc43e627cf582fe3bda4d02e300f6b8f2747c764950d13784c/sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485", size = 2141335, upload-time = "2026-03-02T15:52:51.518Z" }, - { url = "https://files.pythonhosted.org/packages/91/44/f39d063c90f2443e5b46ec4819abd3d8de653893aae92df42a5c4f5843de/sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79", size = 2173095, upload-time = "2026-03-02T15:52:52.79Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b3/f437eaa1cf028bb3c927172c7272366393e73ccd104dcf5b6963f4ab5318/sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd", size = 2154401, upload-time = "2026-03-02T15:49:17.24Z" }, - { url = "https://files.pythonhosted.org/packages/6c/1c/b3abdf0f402aa3f60f0df6ea53d92a162b458fca2321d8f1f00278506402/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f", size = 3274528, upload-time = "2026-03-02T15:50:41.489Z" }, - { url = "https://files.pythonhosted.org/packages/f2/5e/327428a034407651a048f5e624361adf3f9fbac9d0fa98e981e9c6ff2f5e/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b", size = 3279523, upload-time = "2026-03-02T15:53:32.962Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ca/ece73c81a918add0965b76b868b7b5359e068380b90ef1656ee995940c02/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0", size = 3224312, upload-time = "2026-03-02T15:50:42.996Z" }, - { url = "https://files.pythonhosted.org/packages/88/11/fbaf1ae91fa4ee43f4fe79661cead6358644824419c26adb004941bdce7c/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2", size = 3246304, upload-time = "2026-03-02T15:53:34.937Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5fb0deb13930b4f2f698c5541ae076c18981173e27dd00376dbaea7a9c82/sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6", size = 2116565, upload-time = "2026-03-02T15:54:38.321Z" }, - { url = "https://files.pythonhosted.org/packages/95/7e/e83615cb63f80047f18e61e31e8e32257d39458426c23006deeaf48f463b/sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0", size = 2142205, upload-time = "2026-03-02T15:54:39.831Z" }, - { url = "https://files.pythonhosted.org/packages/83/e3/69d8711b3f2c5135e9cde5f063bc1605860f0b2c53086d40c04017eb1f77/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241", size = 3563519, upload-time = "2026-03-02T15:57:52.387Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4f/a7cce98facca73c149ea4578981594aaa5fd841e956834931de503359336/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0", size = 3528611, upload-time = "2026-03-02T16:04:42.097Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7d/5936c7a03a0b0cb0fa0cc425998821c6029756b0855a8f7ee70fba1de955/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3", size = 3472326, upload-time = "2026-03-02T15:57:54.423Z" }, - { url = "https://files.pythonhosted.org/packages/f4/33/cea7dfc31b52904efe3dcdc169eb4514078887dff1f5ae28a7f4c5d54b3c/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b", size = 3478453, upload-time = "2026-03-02T16:04:44.584Z" }, - { url = "https://files.pythonhosted.org/packages/c8/95/32107c4d13be077a9cae61e9ae49966a35dc4bf442a8852dd871db31f62e/sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f", size = 2147209, upload-time = "2026-03-02T15:52:54.274Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d7/1e073da7a4bc645eb83c76067284a0374e643bc4be57f14cc6414656f92c/sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933", size = 2182198, upload-time = "2026-03-02T15:52:55.606Z" }, - { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, -] - [[package]] name = "stack-data" version = "0.6.3" @@ -3108,15 +2981,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/69/06/36d260a695f383345ab5bbc3fd447249594ae2fa8dfd19c533d5ae23f46b/stevedore-5.7.0-py3-none-any.whl", hash = "sha256:fd25efbb32f1abb4c9e502f385f0018632baac11f9ee5d1b70f88cc5e22ad4ed", size = 54483, upload-time = "2026-02-20T13:27:05.561Z" }, ] -[[package]] -name = "tabulate" -version = "0.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, -] - [[package]] name = "terminado" version = "0.18.1" @@ -3271,6 +3135,54 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] +[[package]] +name = "verspec" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/44/8126f9f0c44319b2efc65feaad589cadef4d77ece200ae3c9133d58464d0/verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e", size = 27123, upload-time = "2020-11-30T02:24:09.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31", size = 19640, upload-time = "2020-11-30T02:24:08.387Z" }, +] + +[[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/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/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" }, +] + +[[package]] +name = "wcmatch" +version = "10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421, upload-time = "2025-06-22T19:14:02.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854, upload-time = "2025-06-22T19:14:00.978Z" }, +] + [[package]] name = "wcwidth" version = "0.6.0" @@ -3306,12 +3218,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c wheels = [ { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, ] - -[[package]] -name = "zipp" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, -]