From aac0deda8acb79936ec83eeb8a5b46e196b21aeb Mon Sep 17 00:00:00 2001 From: Nick van der Burgt Date: Wed, 20 May 2026 14:16:17 +0200 Subject: [PATCH 01/13] Add initial version of openadr-plugin --- .github/issue-branch.yml | 2 + .github/workflows/lint-and-test.yml | 29 + .gitignore | 91 + .pre-commit-config.yaml | 47 + .python-version | 1 + Dockerfile | 10 + README.md | 131 + conftest.py | 60 + docker-compose.yml | 147 + .../.gitignore | 2 + .../README.md | 202 + .../docker-compose.yml | 180 + .../keycloak/realm.json | 239 ++ .../python/README.md | 11 + .../python/requirements.txt | 1 + .../python/seed_events.py | 146 + .../python/settings.py | 29 + .../python/trigger_fetch.py | 64 + flexmeasures_openadr3/__init__.py | 97 + flexmeasures_openadr3/models/__init__.py | 27 + flexmeasures_openadr3/models/forms.py | 122 + flexmeasures_openadr3/models/jobs.py | 11 + flexmeasures_openadr3/models/storage.py | 115 + flexmeasures_openadr3/models/views.py | 18 + flexmeasures_openadr3/py.typed | 1 + flexmeasures_openadr3/ui/__init__.py | 0 .../ui/static/css/ven-client-field-help.css | 57 + .../templates/base_flexmeasures_openadr3.html | 6 + .../flexmeasures_oadr3_dashboard.html | 67 + ...easures_oadr3_sensor_configs_overview.html | 108 + .../flexmeasures_oadr3_ven_client_detail.html | 140 + .../flexmeasures_oadr3_ven_client_new.html | 129 + ...measures_oadr3_ven_sensor_config_form.html | 127 + flexmeasures_openadr3/ui/views/__init__.py | 0 flexmeasures_openadr3/ui/views/dashboard.py | 442 ++ flexmeasures_openadr3/utils/__init__.py | 0 flexmeasures_openadr3/utils/blueprints.py | 22 + flexmeasures_openadr3/utils/encryption.py | 82 + flexmeasures_openadr3/utils/events.py | 48 + flexmeasures_openadr3/utils/sensor.py | 574 +++ flexmeasures_openadr3/utils/ven_clients.py | 538 +++ flexmeasures_openadr3/utils/ven_jobs.py | 306 ++ pyproject.toml | 89 + tests/__init__.py | 0 tests/certs/oadr310/vtn/cert.pem | 29 + tests/certs/oadr310/vtn/key.pem | 52 + tests/conftest.py | 373 ++ tests/keycloak_integration_realm.json | 239 ++ tests/test_container/__init__.py | 0 tests/test_container/conftest.py | 262 ++ .../flexmeasures_test_postgres_container.py | 75 + tests/test_container/integration_types.py | 26 + .../oadr310_vtn_test_container.py | 225 + tests/test_container/openadr_vtn_setup.py | 127 + tests/test_ven_client_repository.py | 181 + tests/test_ven_fetch_e2e.py | 172 + tests/test_ven_fetch_jobs.py | 160 + uv.lock | 3807 +++++++++++++++++ 58 files changed, 10246 insertions(+) create mode 100644 .github/issue-branch.yml create mode 100644 .github/workflows/lint-and-test.yml create mode 100644 .gitignore create mode 100755 .pre-commit-config.yaml create mode 100644 .python-version create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 conftest.py create mode 100644 docker-compose.yml create mode 100644 examples/capacity-limits-ingestion-walkthrough/.gitignore create mode 100644 examples/capacity-limits-ingestion-walkthrough/README.md create mode 100644 examples/capacity-limits-ingestion-walkthrough/docker-compose.yml create mode 100644 examples/capacity-limits-ingestion-walkthrough/keycloak/realm.json create mode 100644 examples/capacity-limits-ingestion-walkthrough/python/README.md create mode 100644 examples/capacity-limits-ingestion-walkthrough/python/requirements.txt create mode 100644 examples/capacity-limits-ingestion-walkthrough/python/seed_events.py create mode 100644 examples/capacity-limits-ingestion-walkthrough/python/settings.py create mode 100644 examples/capacity-limits-ingestion-walkthrough/python/trigger_fetch.py create mode 100644 flexmeasures_openadr3/__init__.py create mode 100644 flexmeasures_openadr3/models/__init__.py create mode 100644 flexmeasures_openadr3/models/forms.py create mode 100644 flexmeasures_openadr3/models/jobs.py create mode 100644 flexmeasures_openadr3/models/storage.py create mode 100644 flexmeasures_openadr3/models/views.py create mode 100644 flexmeasures_openadr3/py.typed create mode 100644 flexmeasures_openadr3/ui/__init__.py create mode 100644 flexmeasures_openadr3/ui/static/css/ven-client-field-help.css create mode 100755 flexmeasures_openadr3/ui/templates/base_flexmeasures_openadr3.html create mode 100755 flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_dashboard.html create mode 100644 flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_sensor_configs_overview.html create mode 100644 flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_ven_client_detail.html create mode 100644 flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_ven_client_new.html create mode 100644 flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_ven_sensor_config_form.html create mode 100644 flexmeasures_openadr3/ui/views/__init__.py create mode 100644 flexmeasures_openadr3/ui/views/dashboard.py create mode 100644 flexmeasures_openadr3/utils/__init__.py create mode 100644 flexmeasures_openadr3/utils/blueprints.py create mode 100644 flexmeasures_openadr3/utils/encryption.py create mode 100644 flexmeasures_openadr3/utils/events.py create mode 100644 flexmeasures_openadr3/utils/sensor.py create mode 100644 flexmeasures_openadr3/utils/ven_clients.py create mode 100644 flexmeasures_openadr3/utils/ven_jobs.py create mode 100644 pyproject.toml create mode 100644 tests/__init__.py create mode 100644 tests/certs/oadr310/vtn/cert.pem create mode 100644 tests/certs/oadr310/vtn/key.pem create mode 100644 tests/conftest.py create mode 100644 tests/keycloak_integration_realm.json create mode 100644 tests/test_container/__init__.py create mode 100644 tests/test_container/conftest.py create mode 100644 tests/test_container/flexmeasures_test_postgres_container.py create mode 100644 tests/test_container/integration_types.py create mode 100644 tests/test_container/oadr310_vtn_test_container.py create mode 100644 tests/test_container/openadr_vtn_setup.py create mode 100644 tests/test_ven_client_repository.py create mode 100644 tests/test_ven_fetch_e2e.py create mode 100644 tests/test_ven_fetch_jobs.py create mode 100644 uv.lock diff --git a/.github/issue-branch.yml b/.github/issue-branch.yml new file mode 100644 index 0000000..c8cba5e --- /dev/null +++ b/.github/issue-branch.yml @@ -0,0 +1,2 @@ +openDraftPR: true +autoCloseIssue: true diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml new file mode 100644 index 0000000..24be9e6 --- /dev/null +++ b/.github/workflows/lint-and-test.yml @@ -0,0 +1,29 @@ +name: lint-and-test + +on: push + + +jobs: + check: + runs-on: ubuntu-latest + name: "Check (on Python3.11)" + steps: + - uses: actions/setup-python@v4 + with: + python-version: 3.11 + - uses: actions/checkout@v3 + - uses: pre-commit/action@v3.0.0 + + test: + needs: check + runs-on: ubuntu-latest + strategy: + fail-fast: false + name: "Test (on Python 3.11)" + steps: + - uses: actions/setup-python@v4 + with: + python-version: 3.11 + - name: Check out src from Git + uses: actions/checkout@v3 + - run: make test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..322ba95 --- /dev/null +++ b/.gitignore @@ -0,0 +1,91 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Flask stuff: +instance/ +.webassets-cache + +# Sphinx documentation +docs/_build/ + +# IPython +profile_default/ +ipython_config.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + + +# custom project files + +.vscode +*.pickle + +.ipynb_checkpoints/ +notebooks/.ipynb_checkpoints/ + +flexmeasures.log + +.DS_Store + + +docker-compose-data +ci +flexmeasures-instance + +.cursor \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100755 index 0000000..095dd54 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,47 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-added-large-files + - id: check-json + - id: check-symlinks + - id: check-toml + - id: check-xml + - id: check-yaml + - id: detect-private-key + - id: end-of-file-fixer + - id: fix-byte-order-marker + - id: trailing-whitespace + - id: mixed-line-ending +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.9 + hooks: + - id: ruff + - id: ruff-format +- repo: local + hooks: + - id: mypy + name: mypy + description: a hook the runs mypy with uv + entry: uv run mypy + language: system + types: [python] + stages: [pre-commit] +- repo: local + hooks: + - id: ruff-check + name: ruff-check + description: a hook the runs ruff with uv + entry: uv run ruff check + language: system + types: [python] + stages: [pre-commit] +- repo: local + hooks: + - id: ruff-check + name: ruff-check + description: a hook the runs ruff with uv + entry: uv run ruff format + language: system + types: [python] + stages: [pre-commit] diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cd99a5b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM lfenergy/flexmeasures:v0.33.0 + +# FROM flexmeasures-local:latest + +COPY --from=ghcr.io/astral-sh/uv:0.10-python3.12-trixie-slim /usr/local/bin/uv /usr/local/bin/uv + +COPY . /app/plugins/flexmeasures-openadr3 + +# Install the plugin dependencies INTO the existing uv environment +RUN uv pip install --python /app/.venv/bin/python /app/plugins/flexmeasures-openadr3 diff --git a/README.md b/README.md new file mode 100644 index 0000000..69dd052 --- /dev/null +++ b/README.md @@ -0,0 +1,131 @@ +# flexmeasures-openadr3 + +FlexMeasures plugin that connects to an **OpenADR 3 Virtual Top Node (VTN)** as a **Virtual End Node (VEN)** and ingests demand-response capacity signals into FlexMeasures time-series sensors. + +Requires FlexMeasures **≥ v0.33.0** and Python **≥ 3.12**. + +## What is OpenADR? + +[OpenADR](https://www.openadr.org/) (Open Automated Demand Response) is an industry standard for exchanging demand-response signals between a grid operator or aggregator (**VTN — Virtual Top Node**) and flexible assets or sites (**VEN — Virtual End Node**). Events describe when and how much capacity is available or limited, typically as time-bounded intervals with numeric payloads. + +**OpenADR 3.x** modernises the protocol around REST APIs and JSON payloads (compared with the XML-centric OpenADR 2.0b profile). This plugin implements a **VEN client** using the [`openadr3-client`](https://pypi.org/project/openadr3-client/) Open-Source Python library and targets **OpenADR 3.1** VTNs. + +### Related: OpenLEADR + +[OpenLEADR](https://lfenergy.org/projects/openleadr/) is another [LF Energy](https://lfenergy.org/) project. It provides open-source OpenADR implementations, including a **Rust-based OpenADR 3.1 VTN** ([openleadr-rs](https://github.com/OpenLEADR/openleadr-rs)). OpenLEADR plays the **VTN** role; this FlexMeasures plugin plays the **VEN** role and can consume events published by VTNs such as OpenLEADR. + +## Overview + +The plugin adds **configuration UI only** — it does **not** extend the FlexMeasures CLI or REST API. All setup happens through the web dashboard. + +| Capability | Description | +|------------|-------------| +| **VEN clients** | Register connection details to a VTN: base URL, OAuth client credentials (stored encrypted), and scopes. Each VEN client is represented as a FlexMeasures **generic asset** of type `OpenADR VEN`. | +| **Polling schedules** | Per VEN client, define one or more polling schedules. A polling schedule will run once daily at a configured time in UTC and will fetch demand response signals from the OpenADR VTN configured in the VEN client. | +| **Sensor mapping** | For each polling schedule, the plugin creates or links FlexMeasures **sensors** on the VEN asset and writes incoming event values as **beliefs** (15-minute resolution, kW). | + +### Supported signals (current scope) + +Only two OpenADR event payload types are supported as of the current release of this plugin: + +| Payload type | FlexMeasures sensor | Unit | +|--------------|----------------------|------| +| `IMPORT_CAPACITY_LIMIT` | `import-capacity-limit` | kW | +| `EXPORT_CAPACITY_LIMIT` | `export-capacity-limit` | kW | + +Additional OpenADR payload types may be supported in future releases. The ingestion pipeline is limited to these capacity-limit signals for now. + +### Dashboard + +After installation, open the plugin from the FlexMeasures main menu: + +- **Menu label:** OpenADR 3 Configuration +- **URL:** `/flexmeasures-openadr3/dashboard` + +From the dashboard you can create and manage VEN clients, polling schedules, and inspect linked sensors. + +## How it works + +```mermaid +flowchart LR + subgraph config [Configuration UI] + UI[Dashboard] + VC[VEN client] + SC[polling schedule] + UI --> VC + VC --> SC + end + + subgraph fm [FlexMeasures] + GA[Generic asset: OpenADR VEN] + IS[import-capacity-limit sensor] + ES[export-capacity-limit sensor] + SC --> GA + SC --> IS + SC --> ES + end + + subgraph runtime [Scheduled fetch] + RQ[RQ job at UTC trigger time] + VEN[openadr3-client VEN HTTP client] + VTN[(OpenADR 3 VTN)] + RQ --> VEN + VEN -->|GET events by targets| VTN + VEN -->|TimedBelief| IS + VEN -->|TimedBelief| ES + end + + SC -.->|schedules daily| RQ +``` + +1. An administrator configures a **VEN client** (VTN URL, OAuth) and one or more **polling schedulles** with optional OpenADR targets to filter on and a daily UTC poll time. +2. When signals are enabled, the plugin provisions **import** and/or **export capacity limit** sensors on the VEN asset. +3. An **RQ job** runs at the configured time each day (and reschedules itself after each run). The job calls the VTN, filters events for the supported payload types, validates that active intervals do not overlap, and stores values on the mapped sensors with data source `OpenADR 3 VTN`. +4. These sensors can then be coupled to assets within FlexMeasures. The most obvious mapping for the currently supported signals would be for an OpenADR sensor to map to the `site-consumption-capacity` (import capacity limit) and `site-production-capacity` (export capacity limit) on an asset of the `building` type. But other variations may exist. + +## Installation + +1. Add the plugin to your FlexMeasures config via `FLEXMEASURES_PLUGINS` (a list). Use either: + - The filesystem path to this repository, e.g. `/path/to/flexmeasures-plugin-oadr3`, or + - The installed package name: `flexmeasures_openadr3` (after `pip install` / `uv pip install`). + +2. Optionally set `OPENADR_SECRETS_ENCRYPTION_KEY`, if this environment variable is set, this key is used to encrypt secrets (such as oauth credentials) at rest in asset attributes. Optional, if not set, the global flexmeasures `SECRET_KEY` environment variable is used to encrypt secrets.. + +3. Ensure an RQ worker is running on the queue used for fetch jobs (currently the `ingestion` queue), this is the default behaviour when using the FlexMeasures CLI run-worker when running FlexMeasures v0.33.0 or higher. + +4. Restart FlexMeasures and open **OpenADR 3 Configuration** in the menu. + +## Development + +We use pre-commit to keep code quality up. + +Install necessary tools with: + +```bash +pip install pre-commit black flake8 mypy +pre-commit install +``` + +or: + +```bash +make install-for-dev +``` + +Try it: + +```bash +pre-commit run --all-files --show-diff-on-failure +``` + +To inspect RQ jobs, you can use the rq-dashboard package which can be optionally installed as a dev dependency: + +```bash +uv run rq-dashboard --redis-url redis://{USERNAME}:{PASSWORD}@{HOST}:{PORT}/{QUEUE_NAME} +``` + +In the case of the redis queue configured in the `docker-compose.yml`, this would result in the following command: + +```bash +uv run rq-dashboard --redis-url redis://:fm-redis-pass@localhost:6379/0 +``` diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..5a24b3f --- /dev/null +++ b/conftest.py @@ -0,0 +1,60 @@ +"""Top-level pytest configuration (pytest_plugins must live here).""" + +from __future__ import annotations + +import os +from collections.abc import Iterator + +import pytest +from flask import Flask + +from flexmeasures.app import create as create_flexmeasures_app + +from tests.test_container.flexmeasures_test_postgres_container import ( + FlexMeasuresTestPostgres, + FlexMeasuresTestPostgresContainer, +) + +pytest_plugins = [ + "flexmeasures.conftest", +] + + +@pytest.fixture(scope="session") +def integration_test_flexmeasures_postgres() -> Iterator[FlexMeasuresTestPostgres]: + """ + Session-scoped PostgreSQL testcontainer for FlexMeasures during integration tests. + """ + with FlexMeasuresTestPostgresContainer() as postgres_container: + yield postgres_container.get_connection_info() + + +@pytest.fixture(scope="session") +def app( + integration_test_flexmeasures_postgres: FlexMeasuresTestPostgres, +) -> Iterator[Flask]: + """FlexMeasures app with this plugin loaded (overrides flexmeasures.conftest.app).""" + # FlexMeasures reads this env var during create_app() (see read_config in testing mode). + # Setting SQLALCHEMY_DATABASE_URI on the app config afterwards is too late: db.init_app() + # already created a cached engine bound to TestingConfig's localhost default. + os.environ["SQLALCHEMY_TEST_DATABASE_URI"] = ( + integration_test_flexmeasures_postgres.database_uri + ) + + test_app = create_flexmeasures_app( + env="testing", + plugins=["flexmeasures_openadr3"], + ) + test_app.config["OPENADR_SECRETS_ENCRYPTION_KEY"] = "test-openadr-secrets-key" + test_app.config["ALLOW_INSECURE_HTTP_VTN"] = "true" + test_app.config["SERVER_NAME"] = "localhost" + test_app.config["PREFERRED_URL_SCHEME"] = "http" + + ctx = test_app.app_context() + ctx.push() + + yield test_app + + ctx.pop() + + os.environ.pop("SQLALCHEMY_TEST_DATABASE_URI", None) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..170ccb7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,147 @@ +# ------------------------------------------------------------------ +# This runs your local FlexMeasures code in a docker compose stack. +# Two FlexMeasures instances are spun up, one for serving the web +# UI & API, one to work on computation jobs. +# The server is adding a toy account when it starts. +# (user: toy-user@flexmeasures.io, password: toy-password) +# +# Instead of local code (which is useful for development purposes), +# you can also use the official (and stable) FlexMeasures docker image +# (lfenergy/flexmeasures). Replace the two `build` directives with +# an `image` directive. +# ------------------------------------------------------------------ + +services: + dev-db: + image: postgres:17 + ports: + - "5432:5432" + expose: + - 5432 + restart: always + environment: + POSTGRES_DB: fm-dev-db + POSTGRES_USER: fm-dev-db-user + POSTGRES_PASSWORD: fm-dev-db-pass + PGDATA: /var/lib/postgresql/data + volumes: + - ./ci/load-psql-extensions.sql:/docker-entrypoint-initdb.d/load-psql-extensions.sql + - ./docker-compose-data/dev-db:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U fm-dev-db-user -d fm-dev-db"] + interval: 10s + timeout: 5s + retries: 3 + queue-db: + image: redis + restart: always + command: redis-server --loglevel warning --requirepass fm-redis-pass + expose: + - 6379 + ports: + - "6379:6379" + volumes: + - redis-cache:/data + environment: + - REDIS_REPLICATION_MODE=master + mailhog: + image: mailhog/mailhog + ports: + - "1025:1025" + - "8025:8025" + restart: always + server: + build: + context: . + dockerfile: Dockerfile + ports: + - 5001:5000 + depends_on: + - dev-db + - test-db # use -e SQLALCHEMY_TEST_DATABASE_URI=... to exec pytest + - queue-db + - mailhog + restart: on-failure + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5000/api/v3_0/health/ready"] + start_period: 10s + interval: 20s + timeout: 10s + retries: 6 + environment: + SQLALCHEMY_DATABASE_URI: "postgresql://fm-dev-db-user:fm-dev-db-pass@dev-db:5432/fm-dev-db" + SECRET_KEY: notsecret + SECURITY_TOTP_SECRETS: '{"1": "something-secret"}' + FLEXMEASURES_ENV: development + FLEXMEASURES_REDIS_URL: queue-db + FLEXMEASURES_REDIS_PASSWORD: fm-redis-pass + FLEXMEASURES_PLUGINS: "flexmeasures_openadr3" + MAIL_SERVER: mailhog # MailHog mail server + MAIL_PORT: 1025 # MailHog mail port + LOGGING_LEVEL: INFO + volumes: + # A place for config and plugin code, and custom requirements.txt + # The 1st mount point is for running the FlexMeasures CLI, the 2nd for gunicorn + # We use :rw so flexmeasures CLI commands can write log files + - ./flexmeasures-instance/:/usr/var/flexmeasures-instance/:rw + - ./flexmeasures-instance/:/app/instance/:rw + entrypoint: ["/bin/sh", "-c"] + command: + - | + if [ -f /usr/var/flexmeasures-instance/requirements.txt ]; then + pip install --no-cache-dir -r /usr/var/flexmeasures-instance/requirements.txt + fi + flexmeasures db upgrade + if [ ! -f /usr/var/flexmeasures-instance/.toy-account-created ]; then + flexmeasures add toy-account --name 'Docker Toy Account' + touch /usr/var/flexmeasures-instance/.toy-account-created + else + echo "Not creating toy account (already happened once). You can run 'flexmeasures add toy-account' any time." + fi + gunicorn --bind 0.0.0.0:5000 --worker-tmp-dir /dev/shm --workers 2 --threads 4 wsgi:application + worker: + build: + context: . + dockerfile: Dockerfile + depends_on: + - dev-db + - queue-db + - mailhog + restart: on-failure + environment: + SQLALCHEMY_DATABASE_URI: "postgresql://fm-dev-db-user:fm-dev-db-pass@dev-db:5432/fm-dev-db" + FLEXMEASURES_REDIS_URL: queue-db + FLEXMEASURES_REDIS_PASSWORD: fm-redis-pass + SECRET_KEY: notsecret + SECURITY_TOTP_SECRETS: '{"1": "something-secret"}' + FLEXMEASURES_ENV: development + MAIL_SERVER: mailhog # MailHog mail server + MAIL_PORT: 1025 # MailHog mail port + LOGGING_LEVEL: INFO + volumes: + # a place for config and plugin code, and custom requirements.txt + - ./flexmeasures-instance/:/usr/var/flexmeasures-instance/:rw + entrypoint: ["/bin/sh", "-c"] + command: + - | + if [ -f /usr/var/flexmeasures-instance/requirements.txt ]; then + pip install --no-cache-dir -r /usr/var/flexmeasures-instance/requirements.txt + fi + flexmeasures jobs run-worker --name flexmeasures-worker --queue forecasting\|scheduling + test-db: + image: postgres + expose: + - 5432 + ports: + - "5432:5432" + restart: always + environment: + POSTGRES_DB: flexmeasures_test + POSTGRES_USER: flexmeasures_test + POSTGRES_PASSWORD: flexmeasures_test + volumes: + - ./ci/load-psql-extensions.sql:/docker-entrypoint-initdb.d/load-psql-extensions.sql +volumes: + redis-cache: + driver: local + flexmeasures-instance: diff --git a/examples/capacity-limits-ingestion-walkthrough/.gitignore b/examples/capacity-limits-ingestion-walkthrough/.gitignore new file mode 100644 index 0000000..cd8f8a3 --- /dev/null +++ b/examples/capacity-limits-ingestion-walkthrough/.gitignore @@ -0,0 +1,2 @@ +fm-walkthrough-instance/* +!fm-walkthrough-instance/.gitkeep diff --git a/examples/capacity-limits-ingestion-walkthrough/README.md b/examples/capacity-limits-ingestion-walkthrough/README.md new file mode 100644 index 0000000..f31cbe6 --- /dev/null +++ b/examples/capacity-limits-ingestion-walkthrough/README.md @@ -0,0 +1,202 @@ +# Capacity limits walkthrough + +Hands-on example that mirrors the end-to-end test in `tests/test_ven_fetch_e2e.py`: + +1. Start an OpenADR 3.1 VTN (OpenLEADR-rs) with Keycloak OAuth +2. Seed capacity-limit events on the VTN via Python (Business Logic client) +3. Configure a VEN client and polling schedule in the FlexMeasures UI +4. Fetch events and inspect beliefs on the linked sensors + +## Stack + +| Service | URL (host) | Purpose | +|---------|------------|---------| +| FlexMeasures | http://localhost:5002 | Web UI + plugin dashboard | +| OpenLEADR-rs VTN | http://localhost:3000 | Publishes DR events | +| Keycloak | http://localhost:8080 | OAuth for BL and VEN clients | +| PostgreSQL (FM) | localhost:5433 | FlexMeasures database | +| Redis | localhost:6380 | RQ job queue | + +## Prerequisites + +- Docker and Docker Compose +- Python 3.12+ (for the seed script on your host) + +--- + +## Step 1 — Start the stack + +```bash +cd examples/capacity-limits-walkthrough +docker compose up -d --build +``` + +Wait until FlexMeasures is ready (first start runs DB migrations and creates a toy account): + +```bash +docker compose logs -f server +``` + +Look for the gunicorn startup message, then open `http://localhost:5002` to verify the dashboard is visible (this can take up to 2 minutes during initial startup). + +--- + +## Step 2 — Seed capacity-limit events on the VTN + +The plugin has no CLI for VTN administration. Use the Business Logic OAuth client +(`test-client-id`) to create a 24-hour event with 96 fifteen-minute intervals with an import and export capacity limit event from OpenADR 3.1 for this example scenario. + +```bash +cd python +python -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt + +export OAUTHLIB_INSECURE_TRANSPORT=1 +export OAUTHLIB_RELAX_TOKEN_SCOPE=1 +python seed_events.py +``` + +Expected output: + +``` +Created event id=… with 96 intervals. + First interval start (UTC): … + Last interval start (UTC): … +``` + +The event starts **one hour from seed time** and spans 24 hours. Re-run +`seed_events.py` any time you want fresh data. + +--- + +## Step 3 — Log in to FlexMeasures + +Open `http://localhost:5002` and sign in with the toy account created by Docker: + +| Field | Value | +|-------|-------| +| Email | `toy-user@flexmeasures.io` | +| Password | `toy-password` | + +Open **OpenADR 3 Configuration** in the main menu, or go directly to: + +`http://localhost:5002/flexmeasures-openadr3/dashboard` + +--- + +## Step 4 — Create a VEN client + +Click **Add new VEN client** and fill in the form. + +> Use **Docker-internal hostnames** for VTN and OAuth URLs. FlexMeasures resolves +> these from inside the `server` and `worker` containers. + +| Field | Value | +|-------|-------| +| **Name** | `demo-ven` | +| **VTN URL** | `http://openleadr-vtn:3000` | +| **OAuth client ID** | `test-ven-1` | +| **OAuth client secret** | `my-client-secret` | +| **OAuth token URL** | `http://keycloak:8080/realms/integration-test-realm/protocol/openid-connect/token` | +| **Scopes** | *(leave empty — Keycloak default scopes are used)* | + +Click **Create VEN client**. + +These credentials come from `keycloak/realm.json`, this file is human readable and can be used to view the exact configuration of the configured OAUTH clients which communicate with an OpenADR 3.1 VTN. + +--- + +## Step 5 — Add a polling schedule + +On the dashboard, open **demo-ven** → **Add schedule** (or **Polling schedules** → **Add schedule**). + +| Field | Value | +|-------|-------| +| **Name** | `demo-poll` | +| **Targets** | *(leave empty — fetch all events)* | +| **Daily trigger time (UTC)** | See options below | +| **Import capacity limits** | ✓ checked | +| **Export capacity limits** | ✓ checked | + +Click **Save polling schedule**. The plugin creates two sensors on the VEN asset: + +- `import-capacity-limit-demo-poll` +- `export-capacity-limit-demo-poll` + +### When to run the fetch + +The schedule runs **once per day** at the configured UTC time. For this walkthrough you have two options: + +#### Option A — Trigger immediately (recommended)** + +Skip waiting for the daily schedule and run the fetch now: + +```bash +docker compose exec server python /walkthrough/python/trigger_fetch.py +``` + +#### Option B — Wait for the scheduled job** + +Set **Daily trigger time** to a UTC time 2–3 minutes from now, then save. Ensure the +`worker` container is running — it processes the `ingestion` queue where fetch jobs +are enqueued. + +--- + +## Step 6 — Inspect sensor beliefs + +After the fetch completes: + +1. Go to **Polling schedules** for `demo-ven` +2. Click **Import sensor** or **Export sensor** next to `demo-poll` +3. On the sensor page, open the chart / beliefs view + +You should see **96 beliefs** (one per 15-minute interval), sourced from **OpenADR 3 VTN**. +Each belief's `event_start` is the interval start time; values are random kW limits +between 0 and 100 (set by `seed_events.py`). + +To verify from the worker logs: + +```bash +docker compose logs worker | tail -20 +``` + +Look for a line like: + +``` +Fetched DR events for VEN 'demo-ven' … stored 192 beliefs. +``` + +(192 = 96 import + 96 export intervals stored across both sensors.) + +--- + +## Troubleshooting + +| Symptom | Check | +|---------|-------| +| Seed script cannot reach VTN | `docker compose ps` — is `openleadr-vtn` running? Wait ~30s after startup. | +| OAuth errors in seed script | Keycloak at `http://localhost:8080` — realm imported? | +| VEN client save fails | URLs must use Docker service names (`openleadr-vtn`, `keycloak`), not `localhost`. | +| Fetch job never runs | `docker compose ps worker` — worker must be up on the `forecasting` queue. | +| No beliefs after fetch | Re-run `seed_events.py`; event must exist on VTN before fetch. | +| HTTP blocked to VTN | `ALLOW_INSECURE_HTTP_VTN=true` is set in `docker-compose.yml`. | + +## Reference values + +All constants live in `python/settings.py`. OAuth clients are defined in +`keycloak/realm.json`: + +| Client | ID | Secret | Used for | +|--------|----|--------|----------| +| Business Logic | `test-client-id` | `my-client-secret` | `seed_events.py` | +| VEN | `test-ven-1` | `my-client-secret` | FlexMeasures UI | + +## Clean up + +```bash +docker compose down -v +``` + +This removes containers and the FlexMeasures database volume. diff --git a/examples/capacity-limits-ingestion-walkthrough/docker-compose.yml b/examples/capacity-limits-ingestion-walkthrough/docker-compose.yml new file mode 100644 index 0000000..0604474 --- /dev/null +++ b/examples/capacity-limits-ingestion-walkthrough/docker-compose.yml @@ -0,0 +1,180 @@ +# OpenADR 3 capacity-limits walkthrough stack. +# +# Mirrors the integration-test setup used in tests/test_ven_fetch_e2e.py: +# Keycloak (OAuth) + OpenLEADR-rs VTN + FlexMeasures with this plugin. +# +# Prerequisites (from the repository root): +# docker build -t flexmeasures-oadr3:latest . +# +# Then from this directory: +# docker compose up -d --build + +services: + keycloak: + image: quay.io/keycloak/keycloak:25.0.4 + command: start-dev --import-realm + environment: + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + KC_HTTP_ENABLED: "true" + KC_HOSTNAME_STRICT: "false" + ports: + - "8080:8080" + volumes: + - ./keycloak/realm.json:/opt/keycloak/data/import/realm.json + networks: + - oadr3-walkthrough + + vtn-db: + image: postgres:16 + environment: + POSTGRES_USER: openadr + POSTGRES_PASSWORD: openadr + POSTGRES_DB: openadr + networks: + - oadr3-walkthrough + healthcheck: + test: ["CMD-SHELL", "pg_isready -U openadr -d openadr"] + interval: 5s + timeout: 5s + retries: 10 + + openleadr-vtn: + image: ghcr.io/openleadr/openleadr-rs:1779099254-6c5a5cc + platform: linux/amd64 + depends_on: + vtn-db: + condition: service_healthy + keycloak: + condition: service_started + ports: + - "3000:3000" + environment: + OAUTH_TYPE: EXTERNAL + OAUTH_VALID_AUDIENCES: "https://integration.test.elaad.nl," + OAUTH_KEY_TYPE: RSA + OAUTH_JWKS_LOCATION: http://keycloak:8080/realms/integration-test-realm/protocol/openid-connect/certs + OAUTH_TOKEN_URL: http://keycloak:8080/realms/integration-test-realm/protocol/openid-connect/token + DATABASE_URL: postgres://openadr:openadr@vtn-db:5432/openadr + PG_PORT: "5432" + PG_DB: openadr + PG_USER: openadr + PG_PASSWORD: openadr + RUST_LOG: info + networks: + - oadr3-walkthrough + + fm-db: + image: postgres:17 + environment: + POSTGRES_DB: fm-walkthrough-db + POSTGRES_USER: fm-walkthrough-user + POSTGRES_PASSWORD: fm-walkthrough-pass + ports: + - "5433:5432" + volumes: + - fm-db-data:/var/lib/postgresql/data + networks: + - oadr3-walkthrough + healthcheck: + test: ["CMD-SHELL", "pg_isready -U fm-walkthrough-user -d fm-walkthrough-db"] + interval: 10s + timeout: 5s + retries: 5 + + queue-db: + image: redis:7 + command: redis-server --loglevel warning --requirepass fm-redis-pass + ports: + - "6380:6379" + networks: + - oadr3-walkthrough + + server: + image: flexmeasures-oadr3:latest + # build: + # context: ../.. + # dockerfile: Dockerfile + ports: + - "5002:5000" + depends_on: + fm-db: + condition: service_healthy + queue-db: + condition: service_started + openleadr-vtn: + condition: service_started + keycloak: + condition: service_started + restart: on-failure + environment: + SQLALCHEMY_DATABASE_URI: postgresql://fm-walkthrough-user:fm-walkthrough-pass@fm-db:5432/fm-walkthrough-db + SECRET_KEY: notsecret + SECURITY_TOTP_SECRETS: '{"1": "something-secret"}' + FLEXMEASURES_ENV: development + FLEXMEASURES_REDIS_URL: queue-db + FLEXMEASURES_REDIS_PASSWORD: fm-redis-pass + FLEXMEASURES_PLUGINS: flexmeasures_openadr3 + ALLOW_INSECURE_HTTP_VTN: "true" + OAUTHLIB_INSECURE_TRANSPORT: "1" + OAUTHLIB_RELAX_TOKEN_SCOPE: "1" + LOGGING_LEVEL: INFO + volumes: + - fm-data:/usr/var/flexmeasures-instance:rw + - fm-data:/app/instance:rw + - ./python:/walkthrough/python:ro + entrypoint: ["/bin/sh", "-c"] + command: + - | + flexmeasures db upgrade + if [ ! -f /usr/var/flexmeasures-instance/.toy-account-created ]; then + flexmeasures add toy-account --name 'Walkthrough Toy Account' + touch /usr/var/flexmeasures-instance/.toy-account-created + fi + gunicorn --bind 0.0.0.0:5000 --worker-tmp-dir /dev/shm --workers 2 --threads 4 wsgi:application + networks: + - oadr3-walkthrough + + worker: + image: flexmeasures-oadr3:latest + # build: + # context: ../.. + # dockerfile: Dockerfile + depends_on: + fm-db: + condition: service_healthy + queue-db: + condition: service_started + server: + condition: service_started + restart: on-failure + environment: + SQLALCHEMY_DATABASE_URI: postgresql://fm-walkthrough-user:fm-walkthrough-pass@fm-db:5432/fm-walkthrough-db + FLEXMEASURES_REDIS_URL: queue-db + FLEXMEASURES_REDIS_PASSWORD: fm-redis-pass + SECRET_KEY: notsecret + SECURITY_TOTP_SECRETS: '{"1": "something-secret"}' + FLEXMEASURES_ENV: development + FLEXMEASURES_PLUGINS: flexmeasures_openadr3 + ALLOW_INSECURE_HTTP_VTN: "true" + OAUTHLIB_INSECURE_TRANSPORT: "1" + OAUTHLIB_RELAX_TOKEN_SCOPE: "1" + LOGGING_LEVEL: INFO + volumes: + - fm-worker-data:/usr/var/flexmeasures-instance:rw + - fm-worker-data:/app/instance:rw + - ./python:/walkthrough/python:ro + entrypoint: ["/bin/sh", "-c"] + command: + - flexmeasures jobs run-worker --name flexmeasures-worker --queue forecasting\|scheduling + networks: + - oadr3-walkthrough + +networks: + oadr3-walkthrough: + driver: bridge + +volumes: + fm-db-data: + fm-data: + fm-worker-data: diff --git a/examples/capacity-limits-ingestion-walkthrough/keycloak/realm.json b/examples/capacity-limits-ingestion-walkthrough/keycloak/realm.json new file mode 100644 index 0000000..3e33860 --- /dev/null +++ b/examples/capacity-limits-ingestion-walkthrough/keycloak/realm.json @@ -0,0 +1,239 @@ +{ + "realm": "integration-test-realm", + "enabled": true, + "clientScopes": [ + { + "name": "test/BL", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + } + }, + { + "name": "test/VEN", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + } + }, + { + "name": "read_all", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + } + }, + { + "name": "read_targets", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + } + }, + { + "name": "read_ven_objects", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + } + }, + { + "name": "write_programs", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + } + }, + { + "name": "write_events", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + } + }, + { + "name": "write_reports", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + } + }, + { + "name": "write_subscriptions", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + } + }, + { + "name": "write_vens", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + } + }, + { + "name": "write_users", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + } + } + ], + "clients": [ + { + "clientId": "test-client-id", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "my-client-secret", + "protocol": "openid-connect", + "publicClient": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "defaultClientScopes": [ + "profile", + "email", + "test/BL", + "test/VEN", + "read_all", + "read_targets", + "read_ven_objects", + "write_programs", + "write_events", + "write_reports", + "write_subscriptions", + "write_vens", + "write_users" + ], + "protocolMappers": [ + { + "name": "audience", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-mapper", + "consentRequired": false, + "config": { + "included.client.audience": "https://integration.test.elaad.nl", + "id.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "name": "nbf mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-hardcoded-claim-mapper", + "config": { + "claim.value": "1745926210", + "claim.name": "nbf", + "jsonType.label": "long", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "clientId": "test-ven-1", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "my-client-secret", + "protocol": "openid-connect", + "publicClient": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "defaultClientScopes": [ + "profile", + "email", + "test/VEN", + "read_targets", + "read_ven_objects", + "write_reports", + "write_subscriptions", + "write_vens" + ], + "protocolMappers": [ + { + "name": "audience", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-mapper", + "consentRequired": false, + "config": { + "included.client.audience": "https://integration.test.elaad.nl", + "id.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "name": "nbf mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-hardcoded-claim-mapper", + "config": { + "claim.value": "1745926210", + "claim.name": "nbf", + "jsonType.label": "long", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "clientId": "test-ven-2", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "my-client-secret", + "protocol": "openid-connect", + "publicClient": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "defaultClientScopes": [ + "profile", + "email", + "test/VEN", + "read_targets", + "read_ven_objects", + "write_reports", + "write_subscriptions", + "write_vens" + ], + "protocolMappers": [ + { + "name": "audience", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-mapper", + "consentRequired": false, + "config": { + "included.client.audience": "https://integration.test.elaad.nl", + "id.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "name": "nbf mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-hardcoded-claim-mapper", + "config": { + "claim.value": "1745926210", + "claim.name": "nbf", + "jsonType.label": "long", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true" + } + } + ] + } + ] +} diff --git a/examples/capacity-limits-ingestion-walkthrough/python/README.md b/examples/capacity-limits-ingestion-walkthrough/python/README.md new file mode 100644 index 0000000..48308ef --- /dev/null +++ b/examples/capacity-limits-ingestion-walkthrough/python/README.md @@ -0,0 +1,11 @@ +# Python helpers + +Scripts for the [capacity-limits walkthrough](../README.md). + +| Script | Run where | Purpose | +|--------|-----------|---------| +| `seed_events.py` | Host machine | Create a 24h capacity-limit event on the VTN (BL OAuth client) | +| `trigger_fetch.py` | FlexMeasures container | Run the fetch job immediately for `demo-ven` / `demo-poll` | +| `settings.py` | — | Shared URLs and form values | + +See the [main walkthrough README](../README.md) for step-by-step instructions. diff --git a/examples/capacity-limits-ingestion-walkthrough/python/requirements.txt b/examples/capacity-limits-ingestion-walkthrough/python/requirements.txt new file mode 100644 index 0000000..c790d6b --- /dev/null +++ b/examples/capacity-limits-ingestion-walkthrough/python/requirements.txt @@ -0,0 +1 @@ +openadr3-client>=2.0.4 diff --git a/examples/capacity-limits-ingestion-walkthrough/python/seed_events.py b/examples/capacity-limits-ingestion-walkthrough/python/seed_events.py new file mode 100644 index 0000000..ea5eed0 --- /dev/null +++ b/examples/capacity-limits-ingestion-walkthrough/python/seed_events.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +""" +Seed a 24-hour IMPORT/EXPORT_CAPACITY_LIMIT event on the OpenLEADR-rs VTN. + +Uses the Business Logic OAuth client (test-client-id) — the same flow as the +integration test fixture vtn_capacity_limit_event_seed in tests/conftest.py. +""" + +from __future__ import annotations + +import os +import random +import sys +import uuid +from datetime import datetime, timedelta, timezone +from typing import Tuple + +from openadr3_client._models.common.interval_period import IntervalPeriod +from openadr3_client.bl.http_factory import BusinessLogicHttpClientFactory +from openadr3_client.oadr310._bl.client import BusinessLogicClient +from openadr3_client.oadr310.models.event.event import ExistingEvent, Interval, NewEvent +from openadr3_client.oadr310.models.event.event_payload import ( + EventPayload, + EventPayloadDescriptor, + EventPayloadType, +) +from openadr3_client.oadr310.models.program.program import NewProgram +from openadr3_client.oadr310.models.unit import Unit +from openadr3_client.version import OADRVersion + +from settings import ( + BL_OAUTH_CLIENT_ID, + HOST_KEYCLOAK_TOKEN_URL, + HOST_VTN_BASE_URL, + OAUTH_CLIENT_SECRET, +) + + +def create_bl_client() -> BusinessLogicClient: + """Build a Business Logic HTTP client for the walkthrough VTN.""" + os.environ.setdefault("OAUTHLIB_INSECURE_TRANSPORT", "1") + os.environ.setdefault("OAUTHLIB_RELAX_TOKEN_SCOPE", "1") + return BusinessLogicHttpClientFactory.create_http_bl_client( + vtn_base_url=HOST_VTN_BASE_URL, + client_id=BL_OAUTH_CLIENT_ID, + client_secret=OAUTH_CLIENT_SECRET, + token_url=HOST_KEYCLOAK_TOKEN_URL, + scopes=None, + audience=None, + verify_vtn_tls_certificate=False, + allow_insecure_http=True, + version=OADRVersion.OADR_310, + ) # type: ignore[return-value] + + +def seed_capacity_limit_event( + bl_client: BusinessLogicClient, + *, + target: str | None = None, +) -> ExistingEvent: + """ + Create a program and event with IMPORT/EXPORT_CAPACITY_LIMIT payloads. + + The event starts one hour from the current UTC time and spans 24 hours as + 96 consecutive 15-minute intervals — matching tests/test_container/openadr_vtn_setup.py. + """ + event_start = datetime.now(timezone.utc).replace(microsecond=0) + timedelta(hours=1) + event_duration = timedelta(hours=24) + payload_descriptors = ( + EventPayloadDescriptor( + payload_type=EventPayloadType.IMPORT_CAPACITY_LIMIT, + units=Unit.KW, + ), + EventPayloadDescriptor( + payload_type=EventPayloadType.EXPORT_CAPACITY_LIMIT, + units=Unit.KW, + ), + ) + + program = bl_client.programs.create_program( + NewProgram( + program_name=f"walkthrough-capacity-program-{uuid.uuid4().hex[:8]}", + payload_descriptors=payload_descriptors, + ) + ) + + intervals: Tuple[Interval[EventPayload], ...] = () + for i in range(event_duration // timedelta(minutes=15)): + intervals += ( + Interval( + id=i, + interval_period=IntervalPeriod( + start=event_start + timedelta(minutes=i * 15), + duration=timedelta(minutes=15), + ), + payloads=( + EventPayload( + type=EventPayloadType.IMPORT_CAPACITY_LIMIT, + values=(random.uniform(0, 100),), + ), + EventPayload( + type=EventPayloadType.EXPORT_CAPACITY_LIMIT, + values=(random.uniform(0, 100),), + ), + ), + ), + ) + + return bl_client.events.create_event( + NewEvent( + programID=program.id, + event_name=f"walkthrough-capacity-event-{uuid.uuid4().hex[:8]}", + targets=() if target is None else (target,), + payload_descriptors=payload_descriptors, + intervals=intervals, + ) + ) + + +def main() -> None: + print("Connecting to VTN at", HOST_VTN_BASE_URL) + bl_client = create_bl_client() + event = seed_capacity_limit_event(bl_client) + first_start = ( + event.intervals[0].interval_period.start + if event.intervals and event.intervals[0].interval_period + else None + ) + last_start = ( + event.intervals[-1].interval_period.start + if event.intervals and event.intervals[-1].interval_period + else None + ) + print(f"Created event id={event.id} with {len(event.intervals or ())} intervals.") + if first_start and last_start: + print(f" First interval start (UTC): {first_start.isoformat()}") + print(f" Last interval start (UTC): {last_start.isoformat()}") + print("Next: configure the VEN client and polling schedule in the FlexMeasures UI.") + + +if __name__ == "__main__": + try: + main() + except Exception as exc: + print(f"Failed to seed VTN events: {exc}", file=sys.stderr) + raise SystemExit(1) from exc diff --git a/examples/capacity-limits-ingestion-walkthrough/python/settings.py b/examples/capacity-limits-ingestion-walkthrough/python/settings.py new file mode 100644 index 0000000..f4921ba --- /dev/null +++ b/examples/capacity-limits-ingestion-walkthrough/python/settings.py @@ -0,0 +1,29 @@ +"""Connection settings for the capacity-limits walkthrough stack.""" + +KEYCLOAK_REALM = "integration-test-realm" + +# OAuth clients from keycloak/realm.json (same as tests/keycloak_integration_realm.json) +BL_OAUTH_CLIENT_ID = "test-client-id" +VEN_OAUTH_CLIENT_ID = "test-ven-1" +OAUTH_CLIENT_SECRET = "my-client-secret" + +# URLs when running Python scripts on your host machine +HOST_KEYCLOAK_TOKEN_URL = ( + f"http://localhost:8080/realms/{KEYCLOAK_REALM}/protocol/openid-connect/token" +) +HOST_VTN_BASE_URL = "http://localhost:3000" + +# URLs to enter in the FlexMeasures UI (resolved inside the Docker network) +DOCKER_VTN_BASE_URL = "http://openleadr-vtn:3000" +DOCKER_KEYCLOAK_TOKEN_URL = ( + f"http://keycloak:8080/realms/{KEYCLOAK_REALM}/protocol/openid-connect/token" +) + +# Suggested names for the UI walkthrough +VEN_CLIENT_NAME = "demo-ven" +POLLING_SCHEDULE_NAME = "demo-poll" + +# FlexMeasures web UI +FLEXMEASURES_URL = "http://localhost:5002" +FLEXMEASURES_LOGIN_EMAIL = "toy-user@flexmeasures.io" +FLEXMEASURES_LOGIN_PASSWORD = "toy-password" diff --git a/examples/capacity-limits-ingestion-walkthrough/python/trigger_fetch.py b/examples/capacity-limits-ingestion-walkthrough/python/trigger_fetch.py new file mode 100644 index 0000000..98667bd --- /dev/null +++ b/examples/capacity-limits-ingestion-walkthrough/python/trigger_fetch.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +""" +Run the OpenADR fetch job immediately for a configured polling schedule. + +Mirrors the helper used in tests/test_ven_fetch_e2e.py (_run_fetch_job_immediately). +Intended to run inside the FlexMeasures server container: + + docker compose exec server python /walkthrough/python/trigger_fetch.py +""" + +from __future__ import annotations + +import argparse +import sys + +from flexmeasures.app import create as create_flexmeasures_app +from flexmeasures_openadr3.utils.ven_clients import VenClientRepository +from flexmeasures_openadr3.utils.ven_jobs import VenFetchJobScheduler + +from settings import POLLING_SCHEDULE_NAME, VEN_CLIENT_NAME + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Trigger an immediate OpenADR event fetch for a polling schedule." + ) + parser.add_argument( + "--ven-name", + default=VEN_CLIENT_NAME, + help=f"VEN client name (default: {VEN_CLIENT_NAME})", + ) + parser.add_argument( + "--config-name", + default=POLLING_SCHEDULE_NAME, + help=f"Polling schedule name (default: {POLLING_SCHEDULE_NAME})", + ) + args = parser.parse_args() + + app = create_flexmeasures_app() + + with app.app_context(): + repository = VenClientRepository() + scheduler = VenFetchJobScheduler(repository) + ven_client = repository.find_by_name(args.ven_name) + if ven_client is None: + print(f"VEN client '{args.ven_name}' not found.", file=sys.stderr) + raise SystemExit(1) + + config = ven_client.get_sensor_config(args.config_name) + if config is None: + print( + f"Polling schedule '{args.config_name}' not found on VEN '{args.ven_name}'.", + file=sys.stderr, + ) + raise SystemExit(1) + + scheduler._execute(ven_client.id, config.name) + print( + f"Fetch complete for VEN '{args.ven_name}' schedule '{args.config_name}'." + ) + + +if __name__ == "__main__": + main() diff --git a/flexmeasures_openadr3/__init__.py b/flexmeasures_openadr3/__init__.py new file mode 100644 index 0000000..72167af --- /dev/null +++ b/flexmeasures_openadr3/__init__.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TypedDict + +from flask import Blueprint +from flask.blueprints import BlueprintSetupState + +from .utils.blueprints import ensure_bp_routes_are_loaded_fresh + +__version__ = "0.1" + + +""" +The __init__ for the flexmeasures-oadr3 FlexMeasures plugin. + +FlexMeasures registers the BluePrint objects it finds in here. +""" + + +import warnings # NOQA: E402 +from importlib_metadata import version as pkg_version # NOQA: E402 +from packaging.version import Version # NOQA: E402 + +try: + _fm_version = pkg_version("flexmeasures") + if Version(_fm_version) < Version("v0.32.0"): + warnings.warn( + f"flexmeasures-openadr3 requires FlexMeasures >= v0.32.0, " + f"but version {_fm_version} is installed." + ) +except Exception: + pass + + +class PluginSettingSpec(TypedDict): + description: str + level: str + required: bool + default: str + + +__settings__: dict[str, PluginSettingSpec] = { + "OPENADR_SECRETS_ENCRYPTION_KEY": { + "description": "Encryption key for OpenADR secrets in the attributes.", + "level": "debug", + "required": False, + "default": "", + }, + "ALLOW_INSECURE_HTTP_VTN": { + "description": "Allow insecure HTTP connections to VTNs.", + "level": "debug", + "required": False, + "default": "false", + }, +} + +flexmeasures_openadr3_ui_bp: Blueprint = Blueprint( + "flexmeasures-openadr3 UI", + __name__, + template_folder="ui/templates", + static_folder="ui/static", + url_prefix="/flexmeasures-openadr3", +) +ensure_bp_routes_are_loaded_fresh("ui.views.dashboards") +from flexmeasures_openadr3.ui.views import dashboard # noqa: E402,F401 + + +@dataclass(frozen=True, slots=True) +class MenuRegistration: + view_key: str + title: str + icon: str + + +OPENADR_MENU_REGISTRATION = MenuRegistration( + view_key="flexmeasures-openadr3/dashboard", + title="OpenADR 3 Configuration", + icon="bolt", +) + + +@flexmeasures_openadr3_ui_bp.record_once +def register_menu_item(setup_state: BlueprintSetupState) -> None: + app = setup_state.app + registration = OPENADR_MENU_REGISTRATION + app.config["FLEXMEASURES_MENU_LISTED_VIEWS"] = app.config.get( + "FLEXMEASURES_MENU_LISTED_VIEWS", [] + ) + [registration.view_key] + app.config["FLEXMEASURES_MENU_LISTED_VIEW_TITLES"] = { + **app.config.get("FLEXMEASURES_MENU_LISTED_VIEW_TITLES", {}), + registration.view_key: registration.title, + } + app.config["FLEXMEASURES_MENU_LISTED_VIEW_ICONS"] = { + **app.config.get("FLEXMEASURES_MENU_LISTED_VIEW_ICONS", {}), + registration.view_key: registration.icon, + } diff --git a/flexmeasures_openadr3/models/__init__.py b/flexmeasures_openadr3/models/__init__.py new file mode 100644 index 0000000..7b6e5c2 --- /dev/null +++ b/flexmeasures_openadr3/models/__init__.py @@ -0,0 +1,27 @@ +from flexmeasures_openadr3.models.forms import ( + FormValidationErrors, + FormValidationResult, + VenClientFormValues, + VenSensorConfigFormValues, + VenSensorConfigPostValues, +) +from flexmeasures_openadr3.models.jobs import EventActivePeriod +from flexmeasures_openadr3.models.storage import ( + VEN_CLIENT_ATTRIBUTE_KEY, + VenClientAttributePayload, + VenSensorConfigRecord, +) +from flexmeasures_openadr3.models.views import VenSensorConfigOverview + +__all__ = [ + "EventActivePeriod", + "FormValidationErrors", + "FormValidationResult", + "VEN_CLIENT_ATTRIBUTE_KEY", + "VenClientAttributePayload", + "VenClientFormValues", + "VenSensorConfigFormValues", + "VenSensorConfigOverview", + "VenSensorConfigPostValues", + "VenSensorConfigRecord", +] diff --git a/flexmeasures_openadr3/models/forms.py b/flexmeasures_openadr3/models/forms.py new file mode 100644 index 0000000..c02119f --- /dev/null +++ b/flexmeasures_openadr3/models/forms.py @@ -0,0 +1,122 @@ +from __future__ import annotations + +from collections.abc import Iterator, Mapping +from dataclasses import dataclass, field, replace +from typing import Generic, TypeVar + +ValidatedDataT = TypeVar("ValidatedDataT") + + +@dataclass +class FormValidationErrors: + """Field-level validation messages for HTML forms.""" + + _errors: dict[str, str] = field(default_factory=dict) + + def add(self, field_name: str, message: str) -> None: + if field_name not in self._errors: + self._errors[field_name] = message + + def __getattr__(self, name: str) -> str: + return self._errors.get(name, "") + + def __bool__(self) -> bool: + return bool(self._errors) + + def __len__(self) -> int: + return len(self._errors) + + def __iter__(self) -> Iterator[tuple[str, str]]: + return iter(self._errors.items()) + + +@dataclass(frozen=True, slots=True) +class FormValidationResult(Generic[ValidatedDataT]): + data: ValidatedDataT | None + errors: FormValidationErrors + + @property + def is_valid(self) -> bool: + return self.data is not None and not self.errors + + +@dataclass(frozen=True, slots=True) +class VenClientFormValues: + name: str = "" + vtn_url: str = "" + oauth_client_id: str = "" + oauth_client_secret: str = "" + oauth_token_url: str = "" + scopes: str = "" + + @classmethod + def empty(cls) -> VenClientFormValues: + return cls() + + def with_field(self, field_name: str, value: str) -> VenClientFormValues: + return replace(self, **{field_name: value}) + + def with_request_fields( + self, field_names: tuple[str, ...], form: Mapping[str, str] + ) -> VenClientFormValues: + updated = self + for field_name in field_names: + updated = updated.with_field(field_name, form.get(field_name, "").strip()) + return updated + + def to_validation_input(self) -> dict[str, str]: + return { + "name": self.name, + "vtn_url": self.vtn_url, + "oauth_client_id": self.oauth_client_id, + "oauth_client_secret": self.oauth_client_secret, + "oauth_token_url": self.oauth_token_url, + "scopes": self.scopes, + } + + +@dataclass(frozen=True, slots=True) +class VenSensorConfigFormValues: + name: str = "" + targets: str = "" + utc_trigger_time: str = "" + fetch_import_capacity_limits: str = "" + fetch_export_capacity_limits: str = "" + + @classmethod + def empty(cls) -> VenSensorConfigFormValues: + return cls() + + @classmethod + def from_post_values( + cls, post_values: VenSensorConfigPostValues + ) -> VenSensorConfigFormValues: + return cls( + name=post_values.name, + targets=post_values.targets, + utc_trigger_time=post_values.utc_trigger_time, + fetch_import_capacity_limits="on" + if post_values.fetch_import_capacity_limits + else "", + fetch_export_capacity_limits="on" + if post_values.fetch_export_capacity_limits + else "", + ) + + +@dataclass(frozen=True, slots=True) +class VenSensorConfigPostValues: + name: str + targets: str + utc_trigger_time: str + fetch_import_capacity_limits: bool + fetch_export_capacity_limits: bool + + def to_validation_input(self) -> dict[str, str | bool]: + return { + "name": self.name, + "targets": self.targets, + "utc_trigger_time": self.utc_trigger_time, + "fetch_import_capacity_limits": self.fetch_import_capacity_limits, + "fetch_export_capacity_limits": self.fetch_export_capacity_limits, + } diff --git a/flexmeasures_openadr3/models/jobs.py b/flexmeasures_openadr3/models/jobs.py new file mode 100644 index 0000000..161ec5c --- /dev/null +++ b/flexmeasures_openadr3/models/jobs.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime + + +@dataclass(frozen=True, slots=True) +class EventActivePeriod: + start: datetime + end: datetime + event_id: str diff --git a/flexmeasures_openadr3/models/storage.py b/flexmeasures_openadr3/models/storage.py new file mode 100644 index 0000000..0600e56 --- /dev/null +++ b/flexmeasures_openadr3/models/storage.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass + +VEN_CLIENT_ATTRIBUTE_KEY = "ven_client" + + +@dataclass(frozen=True, slots=True) +class VenSensorConfigRecord: + """Serialized polling schedule stored in a VEN asset's JSON attributes.""" + + name: str + targets: tuple[str, ...] = () + utc_trigger_time: str = "" + fetch_import_capacity_limits: bool = False + fetch_export_capacity_limits: bool = False + fetch_events_job_id: str | None = None + + @classmethod + def from_json(cls, data: Mapping[str, object]) -> VenSensorConfigRecord: + raw_targets = data.get("targets", []) + targets: tuple[str, ...] + if isinstance(raw_targets, list): + targets = tuple(str(item) for item in raw_targets) + else: + targets = () + + job_id = data.get("fetch_events_job_id") + return cls( + name=str(data.get("name", "")), + targets=targets, + utc_trigger_time=str(data.get("utc_trigger_time", "") or ""), + fetch_import_capacity_limits=bool( + data.get("fetch_import_capacity_limits", False) + ), + fetch_export_capacity_limits=bool( + data.get("fetch_export_capacity_limits", False) + ), + fetch_events_job_id=str(job_id) if job_id is not None else None, + ) + + def to_json(self) -> dict[str, object]: + payload: dict[str, object] = { + "name": self.name, + "targets": list(self.targets), + "utc_trigger_time": self.utc_trigger_time, + "fetch_import_capacity_limits": self.fetch_import_capacity_limits, + "fetch_export_capacity_limits": self.fetch_export_capacity_limits, + } + if self.fetch_events_job_id is not None: + payload["fetch_events_job_id"] = self.fetch_events_job_id + return payload + + +@dataclass(frozen=True, slots=True) +class VenClientAttributePayload: + """VEN connection settings and polling schedules stored on a generic asset.""" + + vtn_url: str = "" + oauth_client_id: str = "" + oauth_client_secret: str = "" + oauth_token_url: str = "" + scopes: tuple[str, ...] = () + sensor_configs: tuple[VenSensorConfigRecord, ...] = () + + @classmethod + def empty(cls) -> VenClientAttributePayload: + return cls() + + @classmethod + def from_asset_attributes( + cls, attributes: Mapping[str, object] | None + ) -> VenClientAttributePayload: + if attributes is None: + return cls.empty() + + raw_payload = attributes.get(VEN_CLIENT_ATTRIBUTE_KEY, {}) + if not isinstance(raw_payload, Mapping): + return cls.empty() + + raw_scopes = raw_payload.get("scopes", []) + scopes: tuple[str, ...] + if isinstance(raw_scopes, list): + scopes = tuple(str(item) for item in raw_scopes) + else: + scopes = () + + raw_configs = raw_payload.get("sensor_configs", []) + sensor_configs: tuple[VenSensorConfigRecord, ...] = () + if isinstance(raw_configs, list): + sensor_configs = tuple( + VenSensorConfigRecord.from_json(item) + for item in raw_configs + if isinstance(item, Mapping) + ) + + return cls( + vtn_url=str(raw_payload.get("vtn_url", "") or ""), + oauth_client_id=str(raw_payload.get("oauth_client_id", "") or ""), + oauth_client_secret=str(raw_payload.get("oauth_client_secret", "") or ""), + oauth_token_url=str(raw_payload.get("oauth_token_url", "") or ""), + scopes=scopes, + sensor_configs=sensor_configs, + ) + + def to_json(self) -> dict[str, object]: + return { + "vtn_url": self.vtn_url, + "oauth_client_id": self.oauth_client_id, + "oauth_client_secret": self.oauth_client_secret, + "oauth_token_url": self.oauth_token_url, + "scopes": list(self.scopes), + "sensor_configs": [record.to_json() for record in self.sensor_configs], + } diff --git a/flexmeasures_openadr3/models/views.py b/flexmeasures_openadr3/models/views.py new file mode 100644 index 0000000..6f145f6 --- /dev/null +++ b/flexmeasures_openadr3/models/views.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True, slots=True) +class VenSensorConfigOverview: + """Row model for the polling-schedule overview template.""" + + ven_id: int + ven_name: str + config_name: str + targets: tuple[str, ...] + utc_trigger_time: str + fetch_import_capacity_limits: bool + fetch_export_capacity_limits: bool + import_sensor_id: int | None + export_sensor_id: int | None diff --git a/flexmeasures_openadr3/py.typed b/flexmeasures_openadr3/py.typed new file mode 100644 index 0000000..b648ac9 --- /dev/null +++ b/flexmeasures_openadr3/py.typed @@ -0,0 +1 @@ +partial diff --git a/flexmeasures_openadr3/ui/__init__.py b/flexmeasures_openadr3/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flexmeasures_openadr3/ui/static/css/ven-client-field-help.css b/flexmeasures_openadr3/ui/static/css/ven-client-field-help.css new file mode 100644 index 0000000..824957a --- /dev/null +++ b/flexmeasures_openadr3/ui/static/css/ven-client-field-help.css @@ -0,0 +1,57 @@ +.field-label-with-help { + display: inline-flex; + align-items: center; + gap: 0.35rem; +} + +.field-help-icon-wrapper { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.field-help-icon { + width: 1rem; + height: 1rem; + border-radius: 50%; + border: 1px solid #6c757d; + color: #6c757d; + font-size: 0.65rem; + line-height: 1; + font-weight: 700; + font-family: Arial, sans-serif; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: help; +} + +.field-help-tooltip { + position: absolute; + top: calc(100% + 0.4rem); + left: 0; + z-index: 10; + width: 22rem; + max-width: min(22rem, 80vw); + padding: 0.5rem 0.65rem; + border-radius: 0.375rem; + background-color: #212529; + color: #fff; + font-size: 0.8rem; + font-weight: 400; + line-height: 1.35; + white-space: normal; + box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.25); + opacity: 0; + visibility: hidden; + transform: translateY(-0.2rem); + transition: opacity 120ms ease, transform 120ms ease, visibility 120ms ease; +} + +.field-help-icon-wrapper:hover .field-help-tooltip, +.field-help-icon-wrapper:focus-within .field-help-tooltip { + opacity: 1; + visibility: visible; + transform: translateY(0); +} diff --git a/flexmeasures_openadr3/ui/templates/base_flexmeasures_openadr3.html b/flexmeasures_openadr3/ui/templates/base_flexmeasures_openadr3.html new file mode 100755 index 0000000..e12be7b --- /dev/null +++ b/flexmeasures_openadr3/ui/templates/base_flexmeasures_openadr3.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} + +{% block copyright_notice %} +This is a plugin created by Nick van der Burgt. +. +{% endblock copyright_notice %} diff --git a/flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_dashboard.html b/flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_dashboard.html new file mode 100755 index 0000000..44cf647 --- /dev/null +++ b/flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_dashboard.html @@ -0,0 +1,67 @@ +{% extends "base_flexmeasures_openadr3.html" %} + +{% set active_page = "flexmeasures-openadr3/dashboard" %} + +{% block title %} The flexmeasures-openadr3 dashboard {% endblock %} + + +{% block divs %} +
+
+

OpenADR 3 Dashboard

+ Add new VEN client +
+ +
+ VEN clients represent the static connection settings that a Virtual End Node (VEN) + needs to communicate with a Virtual Top Node (VTN) — the VTN URL, OAuth credentials, and + scopes. Each VEN client can have one or more polling schedules that define + what DR events to fetch, when to fetch them, and which FlexMeasures sensors to store the + retrieved capacity-limit beliefs in. +
+ +
+
+
+
+

VEN clients

+
+
+ {% if ven_clients %} + {% for ven_client in ven_clients %} +
+
+
+ +
+ {{ ven_client.sensor_configs | length }} polling schedule{% if ven_client.sensor_configs | length != 1 %}s{% endif %} +
+
+ +
+
+ {% endfor %} + {% else %} +

No VEN clients defined yet. Create one to start fetching DR events from a VTN.

+ {% endif %} +
+
+
+
+
+{% endblock %} diff --git a/flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_sensor_configs_overview.html b/flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_sensor_configs_overview.html new file mode 100644 index 0000000..5611505 --- /dev/null +++ b/flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_sensor_configs_overview.html @@ -0,0 +1,108 @@ +{% extends "base_flexmeasures_openadr3.html" %} + +{% block title %} Polling schedules - {{ ven_client.name }} {% endblock %} + +{% block divs %} +
+
+

Polling schedules for {{ ven_client.name }}

+ +
+ +
+ A polling schedule defines when and what to fetch from the VTN using + the HTTP polling interface. Each schedule runs daily at the configured trigger time, + retrieves DR events matching the specified targets, and stores the resulting + capacity-limit beliefs in FlexMeasures sensors. You can create multiple schedules per + VEN client — for example, to poll different targets or limit types independently. +
+ +
+
+ {% if sensor_configs %} +
+ + + + + + + + + + + + + {% for cfg in sensor_configs %} + + + + + + + + + {% endfor %} + +
NameTrigger (UTC)TargetsLimitsSensors
{{ cfg.config_name }}{{ cfg.utc_trigger_time }}{% if cfg.targets %}{{ cfg.targets | join(", ") }}{% else %}—{% endif %} + {% if cfg.fetch_import_capacity_limits and cfg.fetch_export_capacity_limits %} + import + export + {% elif cfg.fetch_import_capacity_limits %} + import + {% elif cfg.fetch_export_capacity_limits %} + export + {% else %} + — + {% endif %} + +
+ {% if cfg.fetch_import_capacity_limits and cfg.import_sensor_id %} + + Import sensor + + {% endif %} + {% if cfg.fetch_export_capacity_limits and cfg.export_sensor_id %} + + Export sensor + + {% endif %} +
+
+
+ + Edit + +
+ +
+
+
+
+ {% else %} +

No polling schedules defined yet.

+ {% endif %} +
+
+
+{% endblock %} diff --git a/flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_ven_client_detail.html b/flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_ven_client_detail.html new file mode 100644 index 0000000..14e2bf2 --- /dev/null +++ b/flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_ven_client_detail.html @@ -0,0 +1,140 @@ +{% extends "base_flexmeasures_openadr3.html" %} + +{% block title %} VEN client details {% endblock %} + +{% block divs %} + +
+
+
+
+
+

VEN client: {{ ven_client.name }}

+ +
+
+

+ These are the static connection settings that the VEN uses to authenticate with and + reach the VTN. They are shared across all polling schedules for this VEN client. +

+
+
+
+ + + {% if form_errors.name %} +
{{ form_errors.name }}
+ {% endif %} +
+ +
+ + + {% if form_errors.vtn_url %} +
{{ form_errors.vtn_url }}
+ {% endif %} +
+ +
+ + + {% if form_errors.oauth_client_id %} +
{{ form_errors.oauth_client_id }}
+ {% endif %} +
+ +
+ + + {% if form_errors.oauth_client_secret %} +
{{ form_errors.oauth_client_secret }}
+ {% endif %} +
+ +
+ + + {% if form_errors.oauth_token_url %} +
{{ form_errors.oauth_token_url }}
+ {% endif %} +
+ +
+ + + {% if form_errors.scopes %} +
{{ form_errors.scopes }}
+ {% endif %} +
+ +
+ + +
+
+
+
+
+
+
+
+{% endblock %} diff --git a/flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_ven_client_new.html b/flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_ven_client_new.html new file mode 100644 index 0000000..528e10d --- /dev/null +++ b/flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_ven_client_new.html @@ -0,0 +1,129 @@ +{% extends "base_flexmeasures_openadr3.html" %} + +{% block title %} Add new VEN client {% endblock %} + +{% block divs %} + +
+
+
+
+
+

Add new VEN client

+ Back to dashboard +
+
+

+ Configure the static connection settings that the VEN needs to authenticate with and + reach the VTN. These fields rarely change once set up. + After creating the VEN client you can add polling schedules to start fetching DR events. +

+
+
+
+ + + {% if form_errors.name %} +
{{ form_errors.name }}
+ {% endif %} +
+ +
+ + + {% if form_errors.vtn_url %} +
{{ form_errors.vtn_url }}
+ {% endif %} +
+ +
+ + + {% if form_errors.oauth_client_id %} +
{{ form_errors.oauth_client_id }}
+ {% endif %} +
+ +
+ + + {% if form_errors.oauth_client_secret %} +
{{ form_errors.oauth_client_secret }}
+ {% endif %} +
+ +
+ + + {% if form_errors.oauth_token_url %} +
{{ form_errors.oauth_token_url }}
+ {% endif %} +
+ +
+ + + {% if form_errors.scopes %} +
{{ form_errors.scopes }}
+ {% endif %} +
+ +
+ +
+
+
+
+
+
+
+
+{% endblock %} diff --git a/flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_ven_sensor_config_form.html b/flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_ven_sensor_config_form.html new file mode 100644 index 0000000..4fc7284 --- /dev/null +++ b/flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_ven_sensor_config_form.html @@ -0,0 +1,127 @@ +{% extends "base_flexmeasures_openadr3.html" %} + +{% block title %} {{ title }} {% endblock %} + +{% block divs %} + +
+
+
+
+
+
+

{{ title }}

+
VEN client: {{ ven_client.name }}
+
+ Back to dashboard +
+
+

+ Configure when to poll the VTN for DR events, which targets to filter on, and + which capacity-limit types to retrieve. A FlexMeasures sensor is created automatically + for each selected limit type to store the retrieved beliefs. +

+
+
+
+ + + {% if form_errors.name %} +
{{ form_errors.name }}
+ {% endif %} +
+ +
+ + + {% if form_errors.targets %} +
{{ form_errors.targets }}
+ {% endif %} +
+ +
+ + + {% if form_errors.utc_trigger_time %} +
{{ form_errors.utc_trigger_time }}
+ {% endif %} +
+ +
+
Limits to retrieve
+ {% if form_errors.limits %} +
{{ form_errors.limits }}
+ {% endif %} +
+ + +
+
+ + +
+
+ +
+ {% if delete_action %} + + {% else %} +
+ {% endif %} + +
+
+
+ +
+
+
+
+
+{% endblock %} diff --git a/flexmeasures_openadr3/ui/views/__init__.py b/flexmeasures_openadr3/ui/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flexmeasures_openadr3/ui/views/dashboard.py b/flexmeasures_openadr3/ui/views/dashboard.py new file mode 100644 index 0000000..28f380d --- /dev/null +++ b/flexmeasures_openadr3/ui/views/dashboard.py @@ -0,0 +1,442 @@ +from typing import cast + +from flask import abort, flash, redirect, request, url_for +from werkzeug.wrappers import Response +from flask_login import login_required + +from flexmeasures.data import db +from flexmeasures.ui.utils.view_utils import render_flexmeasures_template + +from ... import flexmeasures_openadr3_ui_bp +from ...models.forms import ( + FormValidationErrors, + VenSensorConfigFormValues, + VenSensorConfigPostValues, +) +from ...models.views import VenSensorConfigOverview +from ...utils.encryption import SecretsDecryptionError, SecretsEncryptor +from ...utils.ven_clients import ( + VenClient, + VenClientRepository, + VEN_CLIENT_FORM_FIELDS, + build_ven_client_form_values, + build_ven_sensor_config_form_values, + validate_ven_client_form, + validate_ven_sensor_config_form, +) +from ...utils.events import ACTIVE_OPENADR_EVENTS, SUPPORTED_SIGNAL_NAMES +from ...utils.ven_jobs import VenFetchJobScheduler + + +def _build_repository() -> VenClientRepository: + """Create a repository only when a request/app context is active.""" + return VenClientRepository() + + +def _build_job_scheduler(repository: VenClientRepository) -> VenFetchJobScheduler: + """Create a scheduler bound to the current request repository.""" + return VenFetchJobScheduler(repository) + + +def _utc_trigger_time_from_request(raw: str) -> str: + """Normalize HTML time input values to HH:MM:SS for validation. + + Browsers may submit HH:MM when seconds are zero; the form validator + expects three colon-separated parts. + """ + stripped = raw.strip() + if not stripped: + return "" + parts = stripped.split(":") + if len(parts) == 2 and all(p.isdigit() for p in parts): + hour, minute = int(parts[0]), int(parts[1]) + if 0 <= hour <= 23 and 0 <= minute <= 59: + return f"{hour:02d}:{minute:02d}:00" + return stripped + + +def _sensor_config_post_values_from_request() -> VenSensorConfigPostValues: + return VenSensorConfigPostValues( + name=request.form.get("name", "").strip(), + targets=request.form.get("targets", "").strip(), + utc_trigger_time=_utc_trigger_time_from_request( + request.form.get("utc_trigger_time", "") + ), + fetch_import_capacity_limits=request.form.get("fetch_import_capacity_limits") + == "on", + fetch_export_capacity_limits=request.form.get("fetch_export_capacity_limits") + == "on", + ) + + +def _decrypt_ven_client_credentials_for_form(ven_client: VenClient) -> VenClient: + """Decrypt VEN credentials for UI rendering. + + If credentials are already plaintext (legacy records), keep them unchanged. + """ + secrets_encryptor = SecretsEncryptor.from_current_app() + try: + ven_client.oauth_client_id = secrets_encryptor.decrypt( + ven_client.oauth_client_id + ) + except SecretsDecryptionError: + pass + try: + ven_client.oauth_client_secret = secrets_encryptor.decrypt( + ven_client.oauth_client_secret + ) + except SecretsDecryptionError: + pass + return ven_client + + +@flexmeasures_openadr3_ui_bp.route("/") +@flexmeasures_openadr3_ui_bp.route("/dashboard") +@login_required +def dashboard() -> str: + """Render the OpenADR 3 dashboard.""" + ven_client_repository = _build_repository() + active_events = tuple( + event + for event in ACTIVE_OPENADR_EVENTS + if all( + pd.payload_type in SUPPORTED_SIGNAL_NAMES + for pd in event.payload_descriptors or () + ) + ) + + return cast( + str, + render_flexmeasures_template( + "flexmeasures_oadr3_dashboard.html", + ven_clients=ven_client_repository.list_ven_clients(), + active_events=active_events, + ), + ) + + +@flexmeasures_openadr3_ui_bp.route("/ven-clients//sensor-configs") +@login_required +def sensor_configs_overview(ven_id: int) -> str: + """Render an overview of sensor configurations for one VEN client.""" + ven_client_repository = _build_repository() + ven_client = ven_client_repository.find_by_id(ven_id) + if ven_client is None: + abort(404) + + sensor_configs = [ + VenSensorConfigOverview( + ven_id=ven_client.id, + ven_name=ven_client.name, + config_name=cfg.name, + targets=tuple(cfg.targets), + utc_trigger_time=cfg.utc_trigger_time, + fetch_import_capacity_limits=cfg.fetch_import_capacity_limits, + fetch_export_capacity_limits=cfg.fetch_export_capacity_limits, + import_sensor_id=cfg.import_sensor.id if cfg.import_sensor else None, + export_sensor_id=cfg.export_sensor.id if cfg.export_sensor else None, + ) + for cfg in ven_client.sensor_configs + ] + + return cast( + str, + render_flexmeasures_template( + "flexmeasures_oadr3_sensor_configs_overview.html", + ven_client=ven_client, + sensor_configs=sensor_configs, + ), + ) + + +@flexmeasures_openadr3_ui_bp.route("/ven-clients/new", methods=["GET", "POST"]) +@login_required +def ven_client_new() -> str | Response: + """Create a new VEN client (shared fields only).""" + ven_client_repository = _build_repository() + field_values = build_ven_client_form_values() + errors = FormValidationErrors() + + if request.method == "POST": + field_values = field_values.with_request_fields( + VEN_CLIENT_FORM_FIELDS, request.form + ) + validation = validate_ven_client_form( + field_values=field_values, + ven_client_repository=ven_client_repository, + ) + errors = validation.errors + + if validation.is_valid: + assert validation.data is not None + ven_client = ven_client_repository.create(validation.data) + db.session.commit() + flash(f"VEN client '{ven_client.name}' created.") + return redirect(url_for(".dashboard")) + + return cast( + str, + render_flexmeasures_template( + "flexmeasures_oadr3_ven_client_new.html", + form_values=field_values, + form_errors=errors, + ), + ) + + +@flexmeasures_openadr3_ui_bp.route("/ven-clients/") +@login_required +def ven_client_detail(ven_id: int) -> str: + """Render the detail page for a VEN client.""" + ven_client_repository = _build_repository() + ven_client = ven_client_repository.find_by_id(ven_id) + if ven_client is None: + abort(404) + ven_client = _decrypt_ven_client_credentials_for_form(ven_client) + + return cast( + str, + render_flexmeasures_template( + "flexmeasures_oadr3_ven_client_detail.html", + ven_client=ven_client, + form_values=build_ven_client_form_values(ven_client), + form_errors=FormValidationErrors(), + ), + ) + + +@flexmeasures_openadr3_ui_bp.route("/ven-clients//update", methods=["POST"]) +@login_required +def ven_client_update(ven_id: int) -> str | Response: + """Update an existing VEN client (shared fields only).""" + ven_client_repository = _build_repository() + ven_client = ven_client_repository.find_by_id(ven_id) + if ven_client is None: + abort(404) + + field_values = build_ven_client_form_values(ven_client).with_request_fields( + VEN_CLIENT_FORM_FIELDS, request.form + ) + validation = validate_ven_client_form( + field_values, + ven_client_repository=ven_client_repository, + current_name=ven_client.name, + ) + if not validation.is_valid: + return cast( + str, + render_flexmeasures_template( + "flexmeasures_oadr3_ven_client_detail.html", + ven_client=ven_client, + form_values=field_values, + form_errors=validation.errors, + ), + ) + + assert validation.data is not None + ven_client = ven_client_repository.update(ven_client, validation.data) + db.session.commit() + flash(f"VEN client '{ven_client.name}' updated.") + return redirect(url_for(".ven_client_detail", ven_id=ven_client.id)) + + +@flexmeasures_openadr3_ui_bp.route("/ven-clients//delete", methods=["POST"]) +@login_required +def ven_client_delete(ven_id: int) -> Response: + """Delete a VEN client and all its scheduled fetch jobs.""" + ven_client_repository = _build_repository() + job_scheduler = _build_job_scheduler(ven_client_repository) + ven_client = ven_client_repository.find_by_id(ven_id) + if ven_client is None: + abort(404) + + job_scheduler.delete_all(ven_client) + ven_client_repository.delete(ven_client) + db.session.commit() + flash(f"VEN client '{ven_client.name}' deleted.") + return redirect(url_for(".dashboard")) + + +@flexmeasures_openadr3_ui_bp.route( + "/ven-clients//sensor-configs/new", methods=["GET"] +) +@login_required +def ven_client_sensor_config_new(ven_id: int) -> str: + """Render the 'new polling schedule' form for a VEN client.""" + ven_client_repository = _build_repository() + ven_client = ven_client_repository.find_by_id(ven_id) + if ven_client is None: + abort(404) + + return cast( + str, + render_flexmeasures_template( + "flexmeasures_oadr3_ven_sensor_config_form.html", + ven_client=ven_client, + form_values=build_ven_sensor_config_form_values(ven_client), + form_errors=FormValidationErrors(), + form_action=url_for( + ".ven_client_sensor_config_create", ven_id=ven_client.id + ), + title="Add polling schedule", + ), + ) + + +@flexmeasures_openadr3_ui_bp.route( + "/ven-clients//sensor-configs", methods=["POST"] +) +@login_required +def ven_client_sensor_config_create(ven_id: int) -> str | Response: + """Handle form submission for creating a polling schedule.""" + ven_client_repository = _build_repository() + job_scheduler = _build_job_scheduler(ven_client_repository) + ven_client = ven_client_repository.find_by_id(ven_id) + if ven_client is None: + abort(404) + + post_values = _sensor_config_post_values_from_request() + validation = validate_ven_sensor_config_form( + post_values, existing_configs=ven_client.sensor_configs + ) + if validation.is_valid: + assert validation.data is not None + sensor_config = ven_client_repository.append_sensor_config( + ven_client, validation.data + ) + job_scheduler.schedule(ven_client, sensor_config) + db.session.commit() + flash(f"Polling schedule added for VEN client '{ven_client.name}'.") + return redirect(url_for(".dashboard")) + + return cast( + str, + render_flexmeasures_template( + "flexmeasures_oadr3_ven_sensor_config_form.html", + ven_client=ven_client, + form_values=VenSensorConfigFormValues.from_post_values(post_values), + form_errors=validation.errors, + form_action=url_for( + ".ven_client_sensor_config_create", ven_id=ven_client.id + ), + title="Add polling schedule", + ), + ) + + +@flexmeasures_openadr3_ui_bp.route( + "/ven-clients//sensor-configs//delete", + methods=["POST"], +) +@login_required +def ven_client_sensor_config_delete(ven_id: int, config_name: str) -> Response: + """Delete a polling schedule from a VEN client.""" + ven_client_repository = _build_repository() + job_scheduler = _build_job_scheduler(ven_client_repository) + ven_client = ven_client_repository.find_by_id(ven_id) + if ven_client is None: + abort(404) + + sensor_config = ven_client.get_sensor_config(config_name) + if sensor_config is None: + abort(404) + + job_scheduler.delete(sensor_config) + ven_client_repository.delete_sensor_config(ven_client, config_name) + db.session.commit() + flash(f"Polling schedule '{config_name}' deleted.") + return redirect(url_for(".sensor_configs_overview", ven_id=ven_id)) + + +@flexmeasures_openadr3_ui_bp.route( + "/ven-clients//sensor-configs/", + methods=["GET"], +) +@login_required +def ven_client_sensor_config_edit(ven_id: int, config_name: str) -> str: + """Render the 'edit polling schedule' form for a VEN client.""" + ven_client_repository = _build_repository() + ven_client = ven_client_repository.find_by_id(ven_id) + if ven_client is None: + abort(404) + + if ven_client.get_sensor_config(config_name) is None: + abort(404) + + return cast( + str, + render_flexmeasures_template( + "flexmeasures_oadr3_ven_sensor_config_form.html", + ven_client=ven_client, + form_values=build_ven_sensor_config_form_values(ven_client, config_name), + form_errors=FormValidationErrors(), + form_action=url_for( + ".ven_client_sensor_config_update", + ven_id=ven_client.id, + config_name=config_name, + ), + delete_action=url_for( + ".ven_client_sensor_config_delete", + ven_id=ven_client.id, + config_name=config_name, + ), + title="Edit polling schedule", + ), + ) + + +@flexmeasures_openadr3_ui_bp.route( + "/ven-clients//sensor-configs//update", + methods=["POST"], +) +@login_required +def ven_client_sensor_config_update(ven_id: int, config_name: str) -> str | Response: + """Handle form submission for updating a polling schedule.""" + ven_client_repository = _build_repository() + job_scheduler = _build_job_scheduler(ven_client_repository) + ven_client = ven_client_repository.find_by_id(ven_id) + if ven_client is None: + abort(404) + + if ven_client.get_sensor_config(config_name) is None: + abort(404) + + post_values = _sensor_config_post_values_from_request() + validation = validate_ven_sensor_config_form( + post_values, + existing_configs=ven_client.sensor_configs, + current_name=config_name, + ) + if validation.is_valid: + assert validation.data is not None + existing_config = ven_client.get_sensor_config(config_name) + assert existing_config is not None + job_scheduler.delete(existing_config) + updated_config = ven_client_repository.update_sensor_config( + ven_client, config_name, validation.data + ) + job_scheduler.schedule(ven_client, updated_config) + db.session.commit() + flash(f"Polling schedule updated for VEN client '{ven_client.name}'.") + return redirect(url_for(".dashboard")) + + return cast( + str, + render_flexmeasures_template( + "flexmeasures_oadr3_ven_sensor_config_form.html", + ven_client=ven_client, + form_values=VenSensorConfigFormValues.from_post_values(post_values), + form_errors=validation.errors, + form_action=url_for( + ".ven_client_sensor_config_update", + ven_id=ven_client.id, + config_name=config_name, + ), + delete_action=url_for( + ".ven_client_sensor_config_delete", + ven_id=ven_client.id, + config_name=config_name, + ), + title="Edit polling schedule", + ), + ) diff --git a/flexmeasures_openadr3/utils/__init__.py b/flexmeasures_openadr3/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flexmeasures_openadr3/utils/blueprints.py b/flexmeasures_openadr3/utils/blueprints.py new file mode 100644 index 0000000..0dd8ebb --- /dev/null +++ b/flexmeasures_openadr3/utils/blueprints.py @@ -0,0 +1,22 @@ +import sys +import importlib + + +def ensure_bp_routes_are_loaded_fresh(module_name: str) -> None: + """ + Reload a module if it has been loaded before. + It's useful for situations in which some other process has read + the module before, but you need some action to happen which only + happens during module import ― decorators are a good example. + + One use case is pytest, which reads all python code when it collects tests. + In our case, that happens before FlexMeasures' import mechanism + has had a chance to know which blueprints a plugin has. And + Seemingly, the importing code (plugin's __init__) can be imported later + than the imported module (containing @route decorators). + Re-importing helps to get this order right when FlexMeasures reads the + plugin's __init__. + """ + m_name = "flexmeasures_openadr3." + module_name + if m_name in sys.modules: + importlib.reload(sys.modules[m_name]) diff --git a/flexmeasures_openadr3/utils/encryption.py b/flexmeasures_openadr3/utils/encryption.py new file mode 100644 index 0000000..eb37e2f --- /dev/null +++ b/flexmeasures_openadr3/utils/encryption.py @@ -0,0 +1,82 @@ +from flask import current_app + +import base64 +from dataclasses import dataclass + +from cryptography.fernet import Fernet, InvalidToken +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.hkdf import HKDF + + +class SecretsEncryptionError(Exception): + """Raised when secrets encryption/decryption fails.""" + + +class InvalidSecretsEncryptionKey(SecretsEncryptionError): + """Raised when a usable master key cannot be loaded.""" + + +class SecretsDecryptionError(SecretsEncryptionError): + """Raised when a value cannot be decrypted (wrong key or corrupted token).""" + + +_KDF_OUTPUT_BYTES = 32 +_KDF_INFO = b"flexmeasures-openadr3:secrets" + + +@dataclass(frozen=True, slots=True) +class SecretsEncryptor: + """Encrypt/decrypt secrets using a master key plus per-value secret key.""" + + _encryption_key: str + + @staticmethod + def _get_secrets_encryption_key() -> str: + """Retrieve the master key used to protect OpenADR secrets.""" + key = current_app.config.get("OPENADR_SECRETS_ENCRYPTION_KEY") + if isinstance(key, str) and key.strip(): + return key + return current_app.config["SECRET_KEY"] or "" + + @classmethod + def from_current_app(cls) -> "SecretsEncryptor": + encryption_key = cls._get_secrets_encryption_key() + if not isinstance(encryption_key, str) or not encryption_key.strip(): + raise InvalidSecretsEncryptionKey( + "Missing OPENADR_SECRETS_ENCRYPTION_KEY or SECRET_KEY." + ) + return cls(_encryption_key=encryption_key) + + def _fernet_for(self, secret_key: str) -> Fernet: + if not isinstance(secret_key, str) or not secret_key: + raise InvalidSecretsEncryptionKey("secret_key must be a non-empty string.") + + hkdf = HKDF( + algorithm=hashes.SHA256(), + length=_KDF_OUTPUT_BYTES, + salt=secret_key.encode("utf-8"), + info=_KDF_INFO, + ) + derived = hkdf.derive(self._encryption_key.encode("utf-8")) + return Fernet(base64.urlsafe_b64encode(derived)) + + def encrypt(self, value: str) -> str: + """Encrypt a string and return a URL-safe token.""" + if not isinstance(value, str): + raise SecretsEncryptionError("value must be a string.") + + token = self._fernet_for(self._encryption_key).encrypt(value.encode("utf-8")) + return token.decode("utf-8") + + def decrypt(self, token: str) -> str: + """Decrypt a token and return the original string.""" + if not isinstance(token, str): + raise SecretsDecryptionError("token must be a string.") + + try: + raw = self._fernet_for(self._encryption_key).decrypt(token.encode("utf-8")) + except InvalidToken as exc: + raise SecretsDecryptionError( + "Invalid token for the given encryption_key." + ) from exc + return raw.decode("utf-8") diff --git a/flexmeasures_openadr3/utils/events.py b/flexmeasures_openadr3/utils/events.py new file mode 100644 index 0000000..15f1b5a --- /dev/null +++ b/flexmeasures_openadr3/utils/events.py @@ -0,0 +1,48 @@ +from datetime import datetime, timedelta, timezone +import uuid + +from openadr3_client.oadr310.models.event.event_payload import ( + EventPayload, + EventPayloadType, + EventPayloadDescriptor, +) +from openadr3_client.oadr310.models.event.event import ExistingEvent, Interval +from openadr3_client._models.common.interval_period import IntervalPeriod +from openadr3_client.oadr310.models.unit import Unit + +SUPPORTED_SIGNAL_NAMES: tuple[EventPayloadType, ...] = ( + EventPayloadType.IMPORT_CAPACITY_AVAILABLE, + EventPayloadType.EXPORT_CAPACITY_LIMIT, +) + +ACTIVE_OPENADR_EVENTS: tuple[ExistingEvent, ...] = ( + ExistingEvent( + id=str(uuid.uuid4()), + programID="program-1", + event_name="event-1", + created_date_time=datetime.now(tz=timezone.utc), + modification_date_time=datetime.now(tz=timezone.utc), + priority=1, + targets=("site:main", "market_role:prosumer"), + payload_descriptors=( + EventPayloadDescriptor( + payload_type=EventPayloadType.IMPORT_CAPACITY_AVAILABLE, + units=Unit.KW, + ), + ), + intervals=( + Interval( + id=1, + interval_period=IntervalPeriod( + start=datetime.now(tz=timezone.utc), + duration=timedelta(hours=1), + ), + payloads=( + EventPayload( + type=EventPayloadType.IMPORT_CAPACITY_AVAILABLE, values=(200,) + ), + ), + ), + ), + ), +) diff --git a/flexmeasures_openadr3/utils/sensor.py b/flexmeasures_openadr3/utils/sensor.py new file mode 100644 index 0000000..62477c0 --- /dev/null +++ b/flexmeasures_openadr3/utils/sensor.py @@ -0,0 +1,574 @@ +from __future__ import annotations + +from dataclasses import dataclass +from datetime import timedelta +from typing import TYPE_CHECKING, Generic, TypeAlias, TypeVar, cast + +from flask_login import current_user +from sqlalchemy import select, delete +import sqlalchemy as sa + +from flexmeasures.data import db +from flexmeasures.data.models.generic_assets import GenericAsset, GenericAssetType +from flexmeasures.data.models.time_series import Sensor, TimedBelief +from flexmeasures.data.models.audit_log import AssetAuditLog +from flexmeasures.utils.time_utils import get_timezone + +if TYPE_CHECKING: + from flexmeasures_openadr3.utils.ven_clients import VenSensorConfig + +JsonScalar: TypeAlias = int | str | None +JsonValue: TypeAlias = JsonScalar | dict[str, "JsonValue"] | list["JsonValue"] +PrunedValueT = TypeVar("PrunedValueT", bound=JsonValue | None) + + +class RemoveMarker: + """Sentinel indicating a JSON node should be removed from its parent.""" + + __slots__ = () + + def __repr__(self) -> str: + return "REMOVE" + + +REMOVE = RemoveMarker() + + +@dataclass(frozen=True, slots=True) +class PruneResult(Generic[PrunedValueT]): + value: PrunedValueT | RemoveMarker + changed: bool + + @property + def should_remove(self) -> bool: + return self.value is REMOVE + + +IMPORT_CAPACITY_LIMIT_SENSOR_NAME = "import-capacity-limit" +EXPORT_CAPACITY_LIMIT_SENSOR_NAME = "export-capacity-limit" + +VEN_ASSET_TYPE_NAME = "OpenADR VEN" +VEN_ASSET_TYPE_DESCRIPTION = "OpenADR Virtual End Node for demand response signals" +VEN_SENSOR_UNIT = "kW" +VEN_SENSOR_EVENT_RESOLUTION = timedelta(minutes=15) + + +def _prune_flex_config_sensor_refs( + value: JsonValue, sensor_id: int +) -> PruneResult[JsonValue]: + """Recursively remove sensor references from nested flex_model/flex_context JSON structures. + + This function handles deeply nested JSON objects and lists from flex_model and flex_context + JSONB columns. It scans for sensor references in two forms: + - Direct objects: {"sensor": sensor_id_to_remove} + - Lists: [sensor_id_to_remove, ...] in "inflexible-device-sensors" keys + + Args: + value: A JSON-like value (dict, list, int, str, None) from flex_model/flex_context. + Can be arbitrarily nested. + sensor_id: The ID of the sensor to remove references to. + + Returns: + A tuple (pruned_value, changed): + - pruned_value: The value with sensor references removed. Can be: + - `_REMOVE` (sentinel object): Remove this entire entry from parent + - A pruned dict/list/scalar: The value with refs removed + - changed (bool): True if any references were actually removed. + + Example: + >>> value = {"soc-max": {"sensor": 42}, "limit": "10 kW"} + >>> pruned, did_change = _prune_flex_config_sensor_refs(value, sensor_id=42) + >>> pruned + {'limit': '10 kW'} + >>> did_change + True + """ + if isinstance(value, dict): + if set(value.keys()) == {"sensor"} and value.get("sensor") == sensor_id: + return PruneResult(value=REMOVE, changed=True) + + changed = False + pruned_dict: dict[str, JsonValue] = {} + for key, nested in value.items(): + if key == "inflexible-device-sensors" and isinstance(nested, list): + new_list = [entry for entry in nested if entry != sensor_id] + if len(new_list) != len(nested): + changed = True + pruned_dict[key] = new_list + continue + + nested_result = _prune_flex_config_sensor_refs(nested, sensor_id) + changed = changed or nested_result.changed + if nested_result.should_remove: + changed = True + continue + if nested_result.value is not REMOVE: + pruned_dict[key] = cast(JsonValue, nested_result.value) + return PruneResult(value=pruned_dict, changed=changed) + + if isinstance(value, list): + changed = False + pruned_list: list[JsonValue] = [] + for item in value: + item_result = _prune_flex_config_sensor_refs(item, sensor_id) + changed = changed or item_result.changed + if item_result.should_remove: + changed = True + continue + if item_result.value is not REMOVE: + pruned_list.append(cast(JsonValue, item_result.value)) + return PruneResult(value=pruned_list, changed=changed) + + return PruneResult(value=value, changed=False) + + +def _prune_sensors_to_show_refs( + value: list[JsonValue] | None, sensor_id: int +) -> PruneResult[list[JsonValue] | None]: + """Remove sensor references from sensors_to_show JSON list. + + This function handles sensors_to_show lists which support multiple entry formats: + - Bare sensor IDs: [42, 43, ...] + - Grouped sensor IDs: [42, [43, 44], ...] (nested lists) + - Dict entries: [{"sensor": 42, ...}, ...] (delegated to _prune_sensors_to_show_entry) + + Args: + value: The sensors_to_show JSON list (or None). Each entry can be an int, list of ints, or dict. + sensor_id: The ID of the sensor to remove references to. + + Returns: + A tuple (pruned_list, changed): + - pruned_list: The list with sensor references removed (or empty lists filtered out). + Returns None/value unchanged if input is not a list. + - changed (bool): True if any references were actually removed. + + Example: + >>> value = [42, [43, 42], {"sensor": 42}] + >>> pruned, did_change = _prune_sensors_to_show_refs(value, sensor_id=42) + >>> pruned + [[43]] + >>> did_change + True + """ + if not isinstance(value, list): + return PruneResult(value=value, changed=False) + + changed = False + cleaned: list[JsonValue] = [] + + for entry in value: + if isinstance(entry, int): + if entry == sensor_id: + changed = True + continue + cleaned.append(entry) + continue + + if isinstance(entry, list): + new_group = [sid for sid in entry if sid != sensor_id] + if len(new_group) != len(entry): + changed = True + if new_group: + cleaned.append(new_group) + else: + changed = True + continue + + if isinstance(entry, dict): + entry_result = _prune_sensors_to_show_entry(entry, sensor_id) + changed = changed or entry_result.changed + if entry_result.should_remove: + continue + if entry_result.value is not REMOVE: + cleaned.append(cast(JsonValue, entry_result.value)) + continue + + cleaned.append(entry) + + return PruneResult(value=cleaned, changed=changed) + + +def _prune_sensors_to_show_entry( + entry: dict[str, JsonValue], sensor_id: int +) -> PruneResult[dict[str, JsonValue]]: + """Remove sensor references from a single sensors_to_show dict entry. + + Handles three field types within a dict entry: + - "sensor": Direct sensor ID reference → remove if matches + - "sensors": List of sensor IDs → filter out matching IDs + - "plots": List of plot dicts, each may contain "sensor" or "sensors" → recurse + + Args: + entry: A dict from the sensors_to_show list (e.g., {"sensor": 42, "title": "..."}). + sensor_id: The ID of the sensor to remove references to. + + Returns: + A tuple (pruned_entry, changed): + - pruned_entry: Can be: + - `_REMOVE`: Remove this entire entry from parent list + - Modified entry dict: The entry with refs removed + - changed (bool): True if any references were removed. + + Example: + >>> entry = {"sensor": 42, "title": "Power"} + >>> pruned, did_change = _prune_sensors_to_show_entry(entry, sensor_id=42) + >>> pruned is _REMOVE + True + """ + if "sensor" in entry: + if entry.get("sensor") == sensor_id: + return PruneResult(value=REMOVE, changed=True) + return PruneResult(value=entry, changed=False) + + if "sensors" in entry and isinstance(entry["sensors"], list): + new_sensors = [sid for sid in entry["sensors"] if sid != sensor_id] + changed = len(new_sensors) != len(entry["sensors"]) + if not new_sensors: + return PruneResult(value=REMOVE, changed=True) + copied = dict(entry) + copied["sensors"] = new_sensors + return PruneResult(value=copied, changed=changed) + + if "plots" in entry and isinstance(entry["plots"], list): + changed = False + new_plots: list[JsonValue] = [] + for plot in entry["plots"]: + if not isinstance(plot, dict): + new_plots.append(plot) + continue + if plot.get("sensor") == sensor_id: + changed = True + continue + if "sensors" in plot and isinstance(plot["sensors"], list): + new_sensors = [sid for sid in plot["sensors"] if sid != sensor_id] + if len(new_sensors) != len(plot["sensors"]): + changed = True + if not new_sensors: + changed = True + continue + copied_plot = dict(plot) + copied_plot["sensors"] = new_sensors + new_plots.append(copied_plot) + else: + new_plots.append(plot) + + if not new_plots: + return PruneResult(value=REMOVE, changed=True) + copied = dict(entry) + copied["plots"] = new_plots + return PruneResult(value=copied, changed=changed) + + return PruneResult(value=entry, changed=False) + + +def _prune_sensors_to_show_as_kpis_refs( + value: list[JsonValue] | None, sensor_id: int +) -> PruneResult[list[JsonValue] | None]: + """Remove sensor references from sensors_to_show_as_kpis JSON list. + + This function handles sensors_to_show_as_kpis lists which support: + - Bare sensor IDs: [42, 43, ...] + - Dict entries: [{"sensor": 42, "title": "...", "function": "sum"}, ...] + + Args: + value: The sensors_to_show_as_kpis JSON list (or None). Each entry is an int or dict. + sensor_id: The ID of the sensor to remove references to. + + Returns: + A tuple (pruned_list, changed): + - pruned_list: The list with sensor references removed. + Returns None/value unchanged if input is not a list. + - changed (bool): True if any references were actually removed. + + Example: + >>> value = [42, {"sensor": 42, "title": "Temp KPI", "function": "sum"}] + >>> pruned, did_change = _prune_sensors_to_show_as_kpis_refs(value, sensor_id=42) + >>> pruned + [] + >>> did_change + True + """ + if not isinstance(value, list): + return PruneResult(value=value, changed=False) + + changed = False + cleaned: list[JsonValue] = [] + for entry in value: + if isinstance(entry, int) and entry == sensor_id: + changed = True + continue + if isinstance(entry, dict) and entry.get("sensor") == sensor_id: + changed = True + continue + cleaned.append(entry) + + return PruneResult(value=cleaned, changed=changed) + + +@dataclass(frozen=True, slots=True) +class VenSensorPair: + import_sensor: Sensor | None + export_sensor: Sensor | None + + +class VenAssetRepository: + """Handles persistence of VEN GenericAsset and GenericAssetType objects.""" + + def get_or_create_asset_type(self) -> GenericAssetType: + asset_type = db.session.execute( + select(GenericAssetType).filter_by(name=VEN_ASSET_TYPE_NAME) + ).scalar_one_or_none() + + if asset_type is None: + asset_type = GenericAssetType( + name=VEN_ASSET_TYPE_NAME, + description=VEN_ASSET_TYPE_DESCRIPTION, + ) + db.session.add(asset_type) + db.session.flush() + + return asset_type + + def create_asset(self, asset_name: str) -> GenericAsset: + asset_type = self.get_or_create_asset_type() + existing = db.session.execute( + select(GenericAsset).filter_by( + name=asset_name, + generic_asset_type_id=asset_type.id, + ) + ).scalar_one_or_none() + + if existing is not None: + raise ValueError(f"Asset with name '{asset_name}' already exists.") + + asset = GenericAsset( + name=asset_name, + generic_asset_type_id=asset_type.id, + account_id=current_user.id, + ) + db.session.add(asset) + db.session.flush() + return asset + + def find_by_id(self, asset_id: int) -> GenericAsset | None: + return db.session.execute( + select(GenericAsset).filter_by(id=asset_id) + ).scalar_one_or_none() + + def find_by_name(self, asset_name: str) -> GenericAsset | None: + return db.session.execute( + select(GenericAsset).filter_by(name=asset_name) + ).scalar_one_or_none() + + def list_all(self) -> list[GenericAsset]: + asset_type = self.get_or_create_asset_type() + return list( + db.session.execute( + select(GenericAsset).filter_by(generic_asset_type_id=asset_type.id) + ).scalars() + ) + + +class VenSensorRepository: + """Handles persistence of FlexMeasures Sensor objects tied to VEN configs.""" + + def _cleanup_sensor_references_in_assets( + self, + sensor_id: int, + sensor_name: str | None = None, + ) -> int: + """Remove references to a sensor in JSONB config fields across assets. + + Returns the number of updated assets. + """ + + vars_json = sa.func.jsonb_build_object("sid", sensor_id) + candidates = db.session.scalars( + sa.select(GenericAsset).where( + sa.or_( + sa.func.jsonb_path_exists( + GenericAsset.flex_model, + "$.**.sensor ? (@ == $sid)", + vars_json, + ), + sa.func.jsonb_path_exists( + GenericAsset.flex_context, + "$.**.sensor ? (@ == $sid)", + vars_json, + ), + sa.func.jsonb_path_exists( + GenericAsset.flex_context, + '$."inflexible-device-sensors"[*] ? (@ == $sid)', + vars_json, + ), + sa.func.jsonb_path_exists( + GenericAsset.sensors_to_show, + "$.**.sensor ? (@ == $sid)", + vars_json, + ), + sa.func.jsonb_path_exists( + GenericAsset.sensors_to_show, + "$.**.sensors[*] ? (@ == $sid)", + vars_json, + ), + sa.func.jsonb_path_exists( + GenericAsset.sensors_to_show, + "$[*] ? (@ == $sid)", + vars_json, + ), + sa.func.jsonb_path_exists( + GenericAsset.sensors_to_show_as_kpis, + "$.**.sensor ? (@ == $sid)", + vars_json, + ), + ) + ) + ).all() + + changed_assets = 0 + for asset in candidates: + flex_model_result = _prune_flex_config_sensor_refs( + asset.flex_model, sensor_id + ) + flex_context_result = _prune_flex_config_sensor_refs( + asset.flex_context, sensor_id + ) + sensors_to_show_result = _prune_sensors_to_show_refs( + asset.sensors_to_show, sensor_id + ) + sensors_to_show_as_kpis_result = _prune_sensors_to_show_as_kpis_refs( + asset.sensors_to_show_as_kpis, sensor_id + ) + + changed = any( + ( + flex_model_result.changed, + flex_context_result.changed, + sensors_to_show_result.changed, + sensors_to_show_as_kpis_result.changed, + ) + ) + if not changed: + continue + + changed_field_events = ( + (flex_model_result.changed, "flex-model"), + (flex_context_result.changed, "flex-context"), + (sensors_to_show_result.changed, "sensors-to-show"), + (sensors_to_show_as_kpis_result.changed, "sensors-to-show-as-kpis"), + ) + for field_changed, field_name in changed_field_events: + if not field_changed: + continue + sensor_label = ( + f"'{sensor_name}': {sensor_id}" if sensor_name else str(sensor_id) + ) + AssetAuditLog.add_record( + asset, + f"Removed sensor reference {sensor_label} from {field_name} (because sensor has been deleted).", + ) + + if flex_model_result.value is not REMOVE: + asset.flex_model = flex_model_result.value + if flex_context_result.value is not REMOVE: + asset.flex_context = flex_context_result.value + if sensors_to_show_result.value is not REMOVE: + asset.sensors_to_show = sensors_to_show_result.value + if sensors_to_show_as_kpis_result.value is not REMOVE: + asset.sensors_to_show_as_kpis = sensors_to_show_as_kpis_result.value + db.session.add(asset) + changed_assets += 1 + + return changed_assets + + def _delete_sensor(self, sensor: Sensor) -> None: + """Delete a sensor and all its time series data. + + Does not commit the session. + Cleans up sensor references in asset JSONB fields. + Creates an audit log. + """ + sensor_name = sensor.name + self._cleanup_sensor_references_in_assets(sensor.id, sensor.name) + db.session.execute(delete(TimedBelief).filter_by(sensor_id=sensor.id)) + AssetAuditLog.add_record( + sensor.generic_asset, f"Deleted sensor '{sensor_name}': {sensor.id}" + ) + db.session.execute(delete(Sensor).filter_by(id=sensor.id)) + + def find_sensor(self, sensor_name: str, asset: GenericAsset) -> Sensor | None: + return db.session.execute( + select(Sensor).filter_by( + name=sensor_name, + generic_asset_id=asset.id, + ) + ).scalar_one_or_none() + + def get_or_create_sensor(self, sensor_name: str, asset: GenericAsset) -> Sensor: + sensor = self.find_sensor(sensor_name, asset) + + if sensor is None: + sensor = Sensor( + name=sensor_name, + generic_asset_id=asset.id, + unit=VEN_SENSOR_UNIT, + timezone=get_timezone(of_user=True).zone or "UTC", + event_resolution=VEN_SENSOR_EVENT_RESOLUTION, + attributes={"ven_client_name": asset.name}, + ) + db.session.add(sensor) + db.session.flush() + + return sensor + + def import_sensor_name(self, config_name: str) -> str: + return f"{IMPORT_CAPACITY_LIMIT_SENSOR_NAME}-{config_name}" + + def export_sensor_name(self, config_name: str) -> str: + return f"{EXPORT_CAPACITY_LIMIT_SENSOR_NAME}-{config_name}" + + def resolve_sensors_for_config( + self, asset: GenericAsset, sensor_config: VenSensorConfig + ) -> VenSensorPair: + """Find existing sensors for a config without creating them.""" + import_sensor = None + export_sensor = None + + if sensor_config.fetch_import_capacity_limits: + import_sensor = self.find_sensor( + self.import_sensor_name(sensor_config.name), asset + ) + if sensor_config.fetch_export_capacity_limits: + export_sensor = self.find_sensor( + self.export_sensor_name(sensor_config.name), asset + ) + + return VenSensorPair(import_sensor=import_sensor, export_sensor=export_sensor) + + def ensure_sensors_for_config( + self, asset: GenericAsset, sensor_config: VenSensorConfig + ) -> VenSensorPair: + """Get or create the sensors required by a config.""" + import_sensor = None + export_sensor = None + + if sensor_config.fetch_import_capacity_limits: + import_sensor = self.get_or_create_sensor( + self.import_sensor_name(sensor_config.name), asset + ) + if sensor_config.fetch_export_capacity_limits: + export_sensor = self.get_or_create_sensor( + self.export_sensor_name(sensor_config.name), asset + ) + + return VenSensorPair(import_sensor=import_sensor, export_sensor=export_sensor) + + def delete_sensors_for_config(self, asset: GenericAsset, config_name: str) -> None: + """Delete both import and export sensors associated with a config name.""" + for sensor_name in ( + self.import_sensor_name(config_name), + self.export_sensor_name(config_name), + ): + sensor = self.find_sensor(sensor_name, asset) + if sensor is not None: + self._delete_sensor(sensor) diff --git a/flexmeasures_openadr3/utils/ven_clients.py b/flexmeasures_openadr3/utils/ven_clients.py new file mode 100644 index 0000000..590bae3 --- /dev/null +++ b/flexmeasures_openadr3/utils/ven_clients.py @@ -0,0 +1,538 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + +from flask import current_app +from openadr3_client.oadr310._ven.client import VirtualEndNodeClient +from openadr3_client.ven.http_factory import VirtualEndNodeHttpClientFactory +from openadr3_client.version import OADRVersion +from pydantic import BaseModel, Field, ValidationError, field_validator +from pydantic_core.core_schema import ValidationInfo + +from flexmeasures.data import db +from flexmeasures.data.models.generic_assets import GenericAsset +from flexmeasures.data.models.time_series import Sensor + +from flexmeasures_openadr3.models.forms import ( + FormValidationErrors, + FormValidationResult, + VenClientFormValues, + VenSensorConfigFormValues, + VenSensorConfigPostValues, +) +from flexmeasures_openadr3.models.storage import ( + VEN_CLIENT_ATTRIBUTE_KEY, + VenClientAttributePayload, + VenSensorConfigRecord, +) +from flexmeasures_openadr3.utils.encryption import SecretsEncryptor +from flexmeasures_openadr3.utils.sensor import VenAssetRepository, VenSensorRepository + + +def _parse_csv_values(raw_value: str) -> list[str]: + """Parse a comma-separated string into a list of trimmed, non-empty values.""" + return [item.strip() for item in raw_value.split(",") if item.strip()] + + +# --------------------------------------------------------------------------- +# Domain models +# --------------------------------------------------------------------------- + + +@dataclass +class VenSensorConfig: + """A polling schedule that defines when and what to fetch from a VTN. + + Each config maps to zero, one, or two FlexMeasures sensors (import/export). + """ + + name: str + targets: list[str] = field(default_factory=list) + utc_trigger_time: str = "" + fetch_import_capacity_limits: bool = False + fetch_export_capacity_limits: bool = False + fetch_events_job_id: str | None = None + import_sensor: Sensor | None = field(default=None, repr=False) + export_sensor: Sensor | None = field(default=None, repr=False) + + def to_record(self) -> VenSensorConfigRecord: + return VenSensorConfigRecord( + name=self.name, + targets=tuple(self.targets), + utc_trigger_time=self.utc_trigger_time, + fetch_import_capacity_limits=self.fetch_import_capacity_limits, + fetch_export_capacity_limits=self.fetch_export_capacity_limits, + fetch_events_job_id=self.fetch_events_job_id, + ) + + @classmethod + def from_record(cls, record: VenSensorConfigRecord) -> VenSensorConfig: + return cls( + name=record.name, + targets=list(record.targets), + utc_trigger_time=record.utc_trigger_time, + fetch_import_capacity_limits=record.fetch_import_capacity_limits, + fetch_export_capacity_limits=record.fetch_export_capacity_limits, + fetch_events_job_id=record.fetch_events_job_id, + ) + + +@dataclass +class VenClient: + """Domain model representing an OpenADR VEN client. + + Wraps a FlexMeasures GenericAsset and provides typed access to VEN-specific + attributes stored in the asset's JSON attributes column. + """ + + asset: GenericAsset = field(repr=False) + vtn_url: str = "" + oauth_client_id: str = "" + oauth_client_secret: str = field(default="", repr=False) + oauth_token_url: str = "" + scopes: list[str] = field(default_factory=list) + sensor_configs: list[VenSensorConfig] = field(default_factory=list) + + @property + def id(self) -> int: + return int(self.asset.id) + + @property + def name(self) -> str: + return str(self.asset.name) + + @name.setter + def name(self, value: str) -> None: + self.asset.name = value + + def get_sensor_config(self, config_name: str) -> VenSensorConfig | None: + return next( + (cfg for cfg in self.sensor_configs if cfg.name == config_name), None + ) + + def create_http_client(self) -> VirtualEndNodeClient: + secrets_encryptor = SecretsEncryptor.from_current_app() + decrypted_oauth_client_id = secrets_encryptor.decrypt(self.oauth_client_id) + decrypted_oauth_client_secret = secrets_encryptor.decrypt( + self.oauth_client_secret + ) + + return VirtualEndNodeHttpClientFactory.create_http_ven_client( + vtn_base_url=self.vtn_url, + client_id=decrypted_oauth_client_id, + client_secret=decrypted_oauth_client_secret, + token_url=self.oauth_token_url, + scopes=self.scopes, + allow_insecure_http=current_app.config.get( + "ALLOW_INSECURE_HTTP_VTN", "false" + ) + == "true", + version=OADRVersion.OADR_310, + ) # type: ignore[return-value] + + def to_attribute_payload(self) -> VenClientAttributePayload: + return VenClientAttributePayload( + vtn_url=self.vtn_url, + oauth_client_id=self.oauth_client_id, + oauth_client_secret=self.oauth_client_secret, + oauth_token_url=self.oauth_token_url, + scopes=tuple(self.scopes), + sensor_configs=tuple(cfg.to_record() for cfg in self.sensor_configs), + ) + + +# --------------------------------------------------------------------------- +# Repository +# --------------------------------------------------------------------------- + + +class VenClientRepository: + """Repository for persisting and retrieving VenClient domain objects.""" + + def __init__(self) -> None: + self._assets = VenAssetRepository() + self._sensors = VenSensorRepository() + self._secrets_encryptor = SecretsEncryptor.from_current_app() + + def _build_sensor_configs( + self, asset: GenericAsset, records: tuple[VenSensorConfigRecord, ...] + ) -> list[VenSensorConfig]: + configs: list[VenSensorConfig] = [] + for record in records: + sensor_config = VenSensorConfig.from_record(record) + sensors = self._sensors.resolve_sensors_for_config(asset, sensor_config) + sensor_config.import_sensor = sensors.import_sensor + sensor_config.export_sensor = sensors.export_sensor + configs.append(sensor_config) + return configs + + def _build_ven_client(self, asset: GenericAsset) -> VenClient: + payload = VenClientAttributePayload.from_asset_attributes(asset.attributes) + + return VenClient( + asset=asset, + vtn_url=payload.vtn_url, + oauth_client_id=payload.oauth_client_id, + oauth_client_secret=payload.oauth_client_secret, + oauth_token_url=payload.oauth_token_url, + scopes=list(payload.scopes), + sensor_configs=self._build_sensor_configs(asset, payload.sensor_configs), + ) + + def _persist_payload( + self, ven_client: VenClient, *, encrypt_oauth_credentials: bool = False + ) -> None: + """Write VEN-specific data back to the asset's attributes column. + + OAuth client id and secret are stored encrypted. Pass *encrypt_oauth_credentials* + True when *ven_client* carries plaintext from a form; pass False when those + fields are unchanged ciphertext loaded from the database (e.g. sensor config + or job id updates). + """ + attributes = dict(ven_client.asset.attributes or {}) + if encrypt_oauth_credentials: + stored_oauth_client_id = self._secrets_encryptor.encrypt( + ven_client.oauth_client_id + ) + stored_oauth_client_secret = self._secrets_encryptor.encrypt( + ven_client.oauth_client_secret + ) + else: + stored_oauth_client_id = ven_client.oauth_client_id + stored_oauth_client_secret = ven_client.oauth_client_secret + + payload = ven_client.to_attribute_payload() + attributes[VEN_CLIENT_ATTRIBUTE_KEY] = VenClientAttributePayload( + vtn_url=payload.vtn_url, + oauth_client_id=stored_oauth_client_id, + oauth_client_secret=stored_oauth_client_secret, + oauth_token_url=payload.oauth_token_url, + scopes=payload.scopes, + sensor_configs=payload.sensor_configs, + ).to_json() + ven_client.asset.attributes = attributes + + def list_ven_clients(self) -> list[VenClient]: + return [self._build_ven_client(asset) for asset in self._assets.list_all()] + + def find_by_id(self, ven_id: int) -> VenClient | None: + asset = self._assets.find_by_id(ven_id) + return self._build_ven_client(asset) if asset else None + + def find_by_name(self, ven_name: str) -> VenClient | None: + asset = self._assets.find_by_name(ven_name) + return self._build_ven_client(asset) if asset else None + + def create(self, form_data: VenClientFormData) -> VenClient: + asset = self._assets.create_asset(form_data.name) + ven_client = VenClient( + asset=asset, + vtn_url=form_data.vtn_url, + oauth_client_id=form_data.oauth_client_id, + oauth_client_secret=form_data.oauth_client_secret, + oauth_token_url=form_data.oauth_token_url, + scopes=form_data.scopes, + ) + self._persist_payload(ven_client, encrypt_oauth_credentials=True) + db.session.flush() + return self._build_ven_client(asset) + + def update(self, ven_client: VenClient, form_data: VenClientFormData) -> VenClient: + ven_client.name = form_data.name + ven_client.vtn_url = form_data.vtn_url + ven_client.oauth_client_id = form_data.oauth_client_id + ven_client.oauth_client_secret = form_data.oauth_client_secret + ven_client.oauth_token_url = form_data.oauth_token_url + ven_client.scopes = form_data.scopes + self._persist_payload(ven_client, encrypt_oauth_credentials=True) + db.session.flush() + return ven_client + + def delete(self, ven_client: VenClient) -> None: + db.session.delete(ven_client.asset) + + def append_sensor_config( + self, ven_client: VenClient, form_data: VenSensorConfigFormData + ) -> VenSensorConfig: + """Append a new sensor config and ensure its sensors exist.""" + if ven_client.get_sensor_config(form_data.name) is not None: + raise ValueError( + f"Sensor config with name '{form_data.name}' already exists." + ) + + sensor_config = VenSensorConfig( + name=form_data.name, + targets=list(form_data.targets), + utc_trigger_time=form_data.utc_trigger_time, + fetch_import_capacity_limits=form_data.fetch_import_capacity_limits, + fetch_export_capacity_limits=form_data.fetch_export_capacity_limits, + ) + + sensors = self._sensors.ensure_sensors_for_config( + ven_client.asset, sensor_config + ) + sensor_config.import_sensor = sensors.import_sensor + sensor_config.export_sensor = sensors.export_sensor + + ven_client.sensor_configs.append(sensor_config) + self._persist_payload(ven_client) + db.session.flush() + return sensor_config + + def update_sensor_config( + self, + ven_client: VenClient, + config_name: str, + form_data: VenSensorConfigFormData, + ) -> VenSensorConfig: + """Update an existing sensor config and ensure its sensors exist.""" + sensor_config = ven_client.get_sensor_config(config_name) + if sensor_config is None: + raise ValueError(f"Sensor config with name '{config_name}' not found.") + + sensor_config.name = form_data.name + sensor_config.targets = list(form_data.targets) + sensor_config.utc_trigger_time = form_data.utc_trigger_time + sensor_config.fetch_import_capacity_limits = ( + form_data.fetch_import_capacity_limits + ) + sensor_config.fetch_export_capacity_limits = ( + form_data.fetch_export_capacity_limits + ) + + sensors = self._sensors.ensure_sensors_for_config( + ven_client.asset, sensor_config + ) + sensor_config.import_sensor = sensors.import_sensor + sensor_config.export_sensor = sensors.export_sensor + + self._persist_payload(ven_client) + db.session.flush() + return sensor_config + + def delete_sensor_config(self, ven_client: VenClient, config_name: str) -> None: + """Remove a sensor config and delete its associated sensors.""" + sensor_config = ven_client.get_sensor_config(config_name) + if sensor_config is None: + raise ValueError(f"Sensor config with name '{config_name}' not found.") + + self._sensors.delete_sensors_for_config(ven_client.asset, config_name) + ven_client.sensor_configs = [ + cfg for cfg in ven_client.sensor_configs if cfg.name != config_name + ] + self._persist_payload(ven_client) + db.session.flush() + + def persist_job_id( + self, ven_client: VenClient, config_name: str, job_id: str | None + ) -> None: + """Store a job ID on a specific sensor config.""" + sensor_config = ven_client.get_sensor_config(config_name) + if sensor_config is None: + return + sensor_config.fetch_events_job_id = job_id + self._persist_payload(ven_client) + db.session.flush() + + +# --------------------------------------------------------------------------- +# Form validation models +# --------------------------------------------------------------------------- + + +class VenClientFormData(BaseModel): + """Validated VEN client form data (shared connection fields).""" + + name: str + vtn_url: str + oauth_client_id: str + oauth_client_secret: str + oauth_token_url: str + scopes: list[str] = Field(default_factory=list) + + @field_validator( + "name", + "vtn_url", + "oauth_client_id", + "oauth_client_secret", + "oauth_token_url", + ) + @classmethod + def _strip_required_string_fields(cls, value: str) -> str: + return value.strip() + + @field_validator("scopes", mode="before") + @classmethod + def _parse_csv_string_fields( + cls, value: str | list[str] | tuple[str, ...] | None + ) -> list[str]: + if value is None: + return [] + if isinstance(value, str): + return _parse_csv_values(value) + if isinstance(value, tuple): + return [item.strip() for item in value if item.strip()] + return [item.strip() for item in value if item.strip()] + + @classmethod + def from_form_values(cls, field_values: VenClientFormValues) -> VenClientFormData: + return cls.model_validate(field_values.to_validation_input()) + + +class VenSensorConfigFormData(BaseModel): + """Validated polling schedule form data.""" + + name: str + targets: list[str] = Field(default_factory=list) + utc_trigger_time: str + fetch_import_capacity_limits: bool = False + fetch_export_capacity_limits: bool = False + + @field_validator("targets", mode="before") + @classmethod + def _parse_targets_csv( + cls, value: str | list[str] | tuple[str, ...] | None + ) -> list[str]: + if value is None: + return [] + if isinstance(value, str): + return _parse_csv_values(value) + if isinstance(value, tuple): + return [item.strip() for item in value if item.strip()] + return [item.strip() for item in value if item.strip()] + + @field_validator("utc_trigger_time") + @classmethod + def _validate_utc_trigger_time(cls, value: str, info: ValidationInfo) -> str: + parsed_value = value.strip() + time_parts = parsed_value.split(":") + if len(time_parts) != 3 or not all(part.isdigit() for part in time_parts): + raise ValueError("Must be in UTC format HH:MM:SS.") + hour, minute, second = (int(part) for part in time_parts) + if hour > 23 or minute > 59 or second > 59: + raise ValueError("Must be in UTC format HH:MM:SS.") + return f"{hour:02d}:{minute:02d}:{second:02d}" + + @classmethod + def from_post_values( + cls, post_values: VenSensorConfigPostValues + ) -> VenSensorConfigFormData: + return cls.model_validate(post_values.to_validation_input()) + + +# --------------------------------------------------------------------------- +# Form helper functions +# --------------------------------------------------------------------------- + +VEN_CLIENT_FORM_FIELDS: tuple[str, ...] = ( + "name", + "vtn_url", + "oauth_client_id", + "oauth_client_secret", + "oauth_token_url", + "scopes", +) + + +def build_ven_client_form_values( + ven_client: VenClient | None = None, +) -> VenClientFormValues: + """Build form values for VEN client create/update screens.""" + if ven_client is None: + return VenClientFormValues.empty() + + return VenClientFormValues( + name=ven_client.name, + vtn_url=ven_client.vtn_url, + oauth_client_id=ven_client.oauth_client_id, + oauth_client_secret=ven_client.oauth_client_secret, + oauth_token_url=ven_client.oauth_token_url, + scopes=", ".join(ven_client.scopes), + ) + + +def build_ven_sensor_config_form_values( + ven_client: VenClient | None = None, + config_name: str | None = None, +) -> VenSensorConfigFormValues: + """Build form values for sensor config create/update screens.""" + if ven_client is None: + return VenSensorConfigFormValues.empty() + + source: VenSensorConfig | None = None + if config_name is not None: + source = ven_client.get_sensor_config(config_name) + elif ven_client.sensor_configs: + source = ven_client.sensor_configs[-1] + + if source is None: + return VenSensorConfigFormValues.empty() + + return VenSensorConfigFormValues( + name=source.name, + targets=", ".join(source.targets), + utc_trigger_time=source.utc_trigger_time, + fetch_import_capacity_limits=( + "on" if source.fetch_import_capacity_limits else "" + ), + fetch_export_capacity_limits=( + "on" if source.fetch_export_capacity_limits else "" + ), + ) + + +def validate_ven_client_form( + field_values: VenClientFormValues, + ven_client_repository: VenClientRepository, + current_name: str | None = None, +) -> FormValidationResult[VenClientFormData]: + """Validate VEN client form values, checking uniqueness.""" + errors = FormValidationErrors() + ven_client_data: VenClientFormData | None = None + + try: + ven_client_data = VenClientFormData.from_form_values(field_values) + except ValidationError as validation_error: + for issue in validation_error.errors(): + field_name = str(issue["loc"][-1]) + errors.add(field_name, issue["msg"]) + else: + assert ven_client_data is not None + existing = ven_client_repository.find_by_name(ven_client_data.name) + if existing and ven_client_data.name != current_name: + errors.add("name", "A VEN client with this name already exists.") + + return FormValidationResult( + data=ven_client_data if not errors else None, errors=errors + ) + + +def validate_ven_sensor_config_form( + post_values: VenSensorConfigPostValues, + existing_configs: list[VenSensorConfig] | None = None, + current_name: str | None = None, +) -> FormValidationResult[VenSensorConfigFormData]: + """Validate a sensor config form payload.""" + errors = FormValidationErrors() + config: VenSensorConfigFormData | None = None + + try: + config = VenSensorConfigFormData.from_post_values(post_values) + except ValidationError as validation_error: + for issue in validation_error.errors(): + field_name = str(issue["loc"][-1]) + errors.add(field_name, issue["msg"]) + else: + assert config is not None + if not ( + config.fetch_import_capacity_limits or config.fetch_export_capacity_limits + ): + errors.add( + "limits", "Select at least one limit type (import and/or export)." + ) + + if existing_configs is not None and config.name != current_name: + if any(cfg.name == config.name for cfg in existing_configs): + errors.add("name", "A sensor config with this name already exists.") + + return FormValidationResult(data=config if not errors else None, errors=errors) diff --git a/flexmeasures_openadr3/utils/ven_jobs.py b/flexmeasures_openadr3/utils/ven_jobs.py new file mode 100644 index 0000000..51d8084 --- /dev/null +++ b/flexmeasures_openadr3/utils/ven_jobs.py @@ -0,0 +1,306 @@ +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from typing import cast + +from flask import current_app +from flexmeasures.data.models.time_series import Sensor, TimedBelief +from flexmeasures.data.services.data_sources import get_or_create_source +from openadr3_client.oadr310._vtn.interfaces.filters import TargetFilter +from openadr3_client.oadr310.models.event.event import ExistingEvent, Interval +from openadr3_client.oadr310.models.event.event_payload import EventPayloadType +from rq import Queue +from rq.exceptions import NoSuchJobError +from rq.job import Job + +from flexmeasures.data import db # type: ignore[import-untyped] +from flexmeasures_openadr3.models.jobs import EventActivePeriod +from flexmeasures_openadr3.utils.ven_clients import ( + VenClient, + VenClientRepository, + VenSensorConfig, +) + +OPENADR_EVENT_SOURCE_NAME = "OpenADR 3 VTN" +OPENADR_EVENT_SOURCE_TYPE = "gateway" +FETCH_EVENTS_QUEUE_NAME = "forecasting" # "ingestion" +SUPPORTED_DR_PAYLOAD_TYPES = ( + EventPayloadType.IMPORT_CAPACITY_LIMIT, + EventPayloadType.EXPORT_CAPACITY_LIMIT, +) + + +class OverlappingDemandResponseEventsError(ValueError): + """Raised when multiple OpenADR events are active for the same period.""" + + +@dataclass(frozen=True, slots=True) +class PayloadTypeSensorMapping: + """Maps supported OpenADR payload types to FlexMeasures sensors.""" + + import_sensor: Sensor | None = None + export_sensor: Sensor | None = None + + @classmethod + def from_sensor_config( + cls, sensor_config: VenSensorConfig + ) -> PayloadTypeSensorMapping: + import_sensor = ( + sensor_config.import_sensor + if sensor_config.fetch_import_capacity_limits + else None + ) + export_sensor = ( + sensor_config.export_sensor + if sensor_config.fetch_export_capacity_limits + else None + ) + return cls(import_sensor=import_sensor, export_sensor=export_sensor) + + def sensor_for(self, payload_type: EventPayloadType) -> Sensor | None: + if payload_type is EventPayloadType.IMPORT_CAPACITY_LIMIT: + return self.import_sensor + if payload_type is EventPayloadType.EXPORT_CAPACITY_LIMIT: + return self.export_sensor + return None + + def __bool__(self) -> bool: + return self.import_sensor is not None or self.export_sensor is not None + + +def _build_job_id(ven_id: int, config_name: str, run_at: datetime) -> str: + return f"oadr3-ven-{ven_id}-{config_name}-fetch-events-{int(run_at.timestamp())}" + + +def _next_daily_trigger_datetime(utc_trigger_time: str) -> datetime: + hour, minute, second = (int(part) for part in utc_trigger_time.split(":")) + now_utc = datetime.now(timezone.utc) + trigger = now_utc.replace(hour=hour, minute=minute, second=second, microsecond=0) + if trigger <= now_utc: + trigger += timedelta(days=1) + return trigger + + +def _fetch_events_queue() -> Queue: + return cast(Queue, current_app.queues[FETCH_EVENTS_QUEUE_NAME]) + + +def _delete_job(job_id: str) -> None: + queue = _fetch_events_queue() + try: + existing_job = Job.fetch(job_id, connection=queue.connection) + except NoSuchJobError: + return + existing_job.delete() + + +def _get_interval_period( + event: ExistingEvent, interval: Interval +) -> tuple[datetime, datetime]: + interval_period = interval.interval_period or event.interval_period + if interval_period is None: + raise ValueError( + f"OpenADR event '{event.id}' interval '{interval.id}' has no interval period." + ) + return interval_period.start, interval_period.start + interval_period.duration + + +def _iter_sensor_event_starts( + start: datetime, end: datetime, event_resolution: timedelta +) -> Iterator[datetime]: + if event_resolution == timedelta(0): + yield start + return + + belief_start = start + while belief_start + event_resolution <= end: + yield belief_start + belief_start += event_resolution + + +def _get_supported_event_payload_types(event: ExistingEvent) -> set[EventPayloadType]: + return { + pd.payload_type + for pd in (event.payload_descriptors or ()) + if pd.payload_type in SUPPORTED_DR_PAYLOAD_TYPES + } + + +def _collect_active_periods(events: list[ExistingEvent]) -> list[EventActivePeriod]: + active_periods: list[EventActivePeriod] = [] + for event in events: + for interval in event.intervals or (): + start, end = _get_interval_period(event, interval) + active_periods.append( + EventActivePeriod(start=start, end=end, event_id=event.id) + ) + return active_periods + + +def _validate_no_overlapping_events(events: list[ExistingEvent]) -> None: + active_periods = sorted( + _collect_active_periods(events), key=lambda period: period.start + ) + for index, period in enumerate(active_periods): + for previous in active_periods[:index]: + if previous.event_id == period.event_id: + continue + if previous.start < period.end and period.start < previous.end: + raise OverlappingDemandResponseEventsError( + f"OpenADR events overlap: " + f"event '{previous.event_id}' active from {previous.start} to {previous.end}, " + f"event '{period.event_id}' active from {period.start} to {period.end}." + ) + + +def _fetch_events(ven_client: VenClient, targets: list[str]) -> list[ExistingEvent]: + client = ven_client.create_http_client() + + target_filter = TargetFilter(targets=targets) + + all_events = client.events.get_events( + target=target_filter, + pagination=None, + program_id=None, + ) + return [event for event in all_events if _get_supported_event_payload_types(event)] + + +def _store_event_payloads( + events: list[ExistingEvent], + sensor_mapping: PayloadTypeSensorMapping, +) -> int: + source = get_or_create_source( + source=OPENADR_EVENT_SOURCE_NAME, + source_type=OPENADR_EVENT_SOURCE_TYPE, + ) + stored_beliefs = 0 + for event in events: + belief_time = event.modification_date_time + for interval in event.intervals or (): + start, end = _get_interval_period(event, interval) + for payload in interval.payloads: + sensor = sensor_mapping.sensor_for(payload.type) + if sensor is None: + continue + if len(payload.values) != 1: + raise ValueError( + f"OpenADR event '{event.id}' interval '{interval.id}' " + f"payload '{payload.type}' has {len(payload.values)} values." + ) + + for belief_start in _iter_sensor_event_starts( + start, end, sensor.event_resolution + ): + db.session.merge( + TimedBelief( + sensor=sensor, + source=source, + event_value=float(payload.values[0]), + event_start=belief_start, + belief_time=belief_time, + ) + ) + stored_beliefs += 1 + db.session.flush() + return stored_beliefs + + +class VenFetchJobScheduler: + """Manages RQ jobs for fetching OpenADR events per sensor config.""" + + def __init__(self, repository: VenClientRepository) -> None: + self._repository = repository + + def schedule( + self, + ven_client: VenClient, + sensor_config: VenSensorConfig, + replace_existing: bool = True, + ) -> Job | None: + """Schedule or reschedule the daily fetch-events job for a sensor config.""" + utc_trigger_time = sensor_config.utc_trigger_time.strip() + if not utc_trigger_time: + return None + + if replace_existing and sensor_config.fetch_events_job_id: + _delete_job(sensor_config.fetch_events_job_id) + + run_at = _next_daily_trigger_datetime(utc_trigger_time) + job_id = _build_job_id(ven_client.id, sensor_config.name, run_at) + queue = _fetch_events_queue() + + current_app.logger.info( + "Scheduling fetch-events job for VEN '%s' (%s) config '%s' at %s.", + ven_client.name, + ven_client.id, + sensor_config.name, + run_at, + ) + + job = queue.enqueue_at( + datetime=run_at, + f=self._execute, + kwargs={"ven_id": ven_client.id, "config_name": sensor_config.name}, + job_id=job_id, + result_ttl=60 * 60 * 24, + ) + + self._repository.persist_job_id(ven_client, sensor_config.name, job.id) + return cast(Job, job) + + def delete(self, sensor_config: VenSensorConfig) -> None: + """Delete the scheduled job for a single sensor config.""" + if sensor_config.fetch_events_job_id: + _delete_job(sensor_config.fetch_events_job_id) + + def delete_all(self, ven_client: VenClient) -> None: + """Delete all scheduled jobs for every sensor config of a VEN client.""" + for sensor_config in ven_client.sensor_configs: + self.delete(sensor_config) + + def _execute(self, ven_id: int, config_name: str) -> None: + """Fetch events for a sensor config and store the resulting beliefs.""" + ven_client = self._repository.find_by_id(ven_id) + if ven_client is None: + current_app.logger.warning( + "Stopping fetch-events job: VEN id '%s' no longer exists.", ven_id + ) + return + + sensor_config = ven_client.get_sensor_config(config_name) + if sensor_config is None: + current_app.logger.warning( + "Stopping fetch-events job: config '%s' no longer exists on VEN '%s' (%s).", + config_name, + ven_client.name, + ven_id, + ) + return + + try: + sensor_mapping = PayloadTypeSensorMapping.from_sensor_config(sensor_config) + if not sensor_mapping: + current_app.logger.warning( + "No sensors configured for VEN '%s' config '%s', skipping.", + ven_client.name, + sensor_config.name, + ) + return + + events = _fetch_events(ven_client, sensor_config.targets) + _validate_no_overlapping_events(events) + stored_beliefs = _store_event_payloads(events, sensor_mapping) + current_app.logger.info( + "Fetched DR events for VEN '%s' (%s) config '%s', stored %s beliefs.", + ven_client.name, + ven_id, + config_name, + stored_beliefs, + ) + db.session.commit() + finally: + self.schedule(ven_client, sensor_config, replace_existing=False) + db.session.commit() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..801bb2c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,89 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "flexmeasures-openadr3" +description = "A plugin which implements OpenADR 3 VEN functionality for FlexMeasures. Allowing for ingestion of ADR signals." +readme = "README.md" +requires-python = ">=3.12" +license = "Apache-2.0" +authors = [ + {name = "Nick van der Burgt", email = "nick.van.der.burgt@elaad.nl"} +] +keywords = ["flexmeasures", "energy flexibility", "openadr", "demand response", "ven"] +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Development Status :: 3 - Alpha", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Typing :: Typed", +] +version = "0.1" +dependencies = [ + "flexmeasures>=0.32.0", + "openadr3-client>=2.0.4", +] + +[project.urls] +Homepage = "https://github.com/elaadnl/flexmeasures-plugin-oadr3" + +[tool.hatch.build.targets.wheel] +packages = ["flexmeasures_openadr3"] + +[tool.hatch.build.targets.sdist] +include = [ + "/flexmeasures_openadr3", + "/flexmeasures_openadr3/py.typed", + "/README.md", +] + +# UV-specific configuration +[tool.uv] +dev-dependencies = [ + # Inherits from [project.optional-dependencies.dev] + # Testing + "pytest>=7.0.0", + "pytest-cov>=4.0.0", + # Linting & formatting + "mypy>=1.0.0", + "ruff>=0.15.11", + # Pre-commit + "pre-commit>=3.0.0", + "types-pytz>=2026.1.1.20260408", + "types-requests>=2.33.0.20260408", + "types-flask>=1.1.6", + "types-click>=7.1.8", + "types-redis>=4.6.0.20241004", + "types-tzlocal>=5.1.0.1", + "types-python-dateutil>=2.9.0.20260408", + "rq-dashboard>=0.8.6", + "fakeredis>=2.35.1", + "types-docker>=7.1.0.20260518", + "testcontainers[keycloak,postgres]>=4.14.2", +] + +# Existing tool configurations (keep these as-is) +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["."] + +[tool.mypy] +python_version = "3.12" +packages = ["flexmeasures_openadr3"] +disallow_untyped_defs = true +disallow_incomplete_defs = true +warn_return_any = true +check_untyped_defs = true + +[[tool.mypy.overrides]] +module = ["flask_login.*", "flexmeasures.*", "tests.*", "testcontainers.*"] +ignore_missing_imports = true +disallow_untyped_defs = false + +[tool.ruff] +# Your ruff config here diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/certs/oadr310/vtn/cert.pem b/tests/certs/oadr310/vtn/cert.pem new file mode 100644 index 0000000..b0238a4 --- /dev/null +++ b/tests/certs/oadr310/vtn/cert.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCTCCAvGgAwIBAgIUFtSO6/PUSl27AYkhqzBoK7hAm5AwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MTIwMzA3NTExNloXDTI2MTIw +MzA3NTExNlowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEAzbebmcxvxTsyYeVSjM0hIZAf84f/QrCvuYU3M0GNLVMU +vNRp4VGmVi9eCa/mz0a9xYnyXujP3LlS2O4SN66TyMT3xP9F6xSTmIv+K2B0GZeT +XCCjiVdNWlm+rlWOi+I9Th4XCpMCDUTIPa1mxJ3KWal/7SwoFbhGkYiuqVH6gjEq +vtmTNamsPXxc+OayMpAbxZc1PQcYAs6bYOgX6uqG6iZb0a9u+DMErtS0t3Aa9MTg +ZtV3ZsxBerlrOq2fCmJzw6NDQHjAw+3wfuut04W0BDhmVuRLYtTn/HGDNWWh2Hip +6pRqpAp1eEOf6hJZOQSVmrtvHpnQoMMQpP2J3MuMHLTxHbh5x93sgkYRtdG1GrnB +ZuxBEDIyj2mOgmf8bZurXI2d3FyyqRgiuKKFfn9iXArIpt9yiGQIyOHVYPnyRSQK +5MLar3xWK0QeaT5SnWMBAuuKve4L+v2lYr94QRXqMEHuYHCafZhJ/+rjb4qys3tM +YS1kDuxo2zcchU7Srk4uyrnTNXSnJaypTVsql4fGtEmgJXvtmjZS3ELm6SwT38/N +qnOR+jztemTti16i375mILvegv5MUGRAbiL4So+NrBkn7AcsfPoRYfnfYdJFWtD+ +p5P2mwB+KJfGR8Z1UiVQhErZo0LI1eP/cpr1cNj82vWhbC0VnLkXoDFpNwfT3qEC +AwEAAaNTMFEwHQYDVR0OBBYEFLvWqMhBiotM9EX8rizjjozbSYtgMB8GA1UdIwQY +MBaAFLvWqMhBiotM9EX8rizjjozbSYtgMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggIBAEt7urhiA07EpZeuSQwKxTOivrX24TrVCz/GOBBxFAu9xRJQ +ID8KYd8picLQOMxiN3adNKxoY3W5oNuAJNrZVMnCUg31RXolSdQmvyXCPOkqKGP3 +5Faod/O9Gh/r+8ctwtpg2zIUbpuIcKYXKQ1MYwBdhVyKfB/I1nKmD8ysTDX5dHWr +YjMVKjeTeKuLkqdXbR6dFbdcf8ZTjLk7d9xaRRn+GaXa9Xn/XGx8S3eXM/RlfijT +3t3CcLfXjpQGl/gB3YXPQty1XXJYEZRwgyMMD0rBUdWaL/INAHdoTOaTlpWJK+gw +fLFovEVeWA6GeZKn+pGZPIS4q7K04H94W2lsGiCqMiRScQ5/ITEGX9SOGKmKFPyz +fe+wNfazpSJTn8rsyGwGGRx/9t5/kVZRBkXJ+GGol8BN+DhKWER70HaSVCCeyy+D +3TsYO3LecJWJzgU74HmSMA7EIs+wfVkX+5sxy8tfl4n0qDGDmN95EhBYmFLGK/h4 +X3ljIWVWghjVaxSWZVLwDCe1wa4qFQS+8bQM4cy7t0l1pXRX2LTd7m9wqeDFo4pN +z5Dqw30LVJvR7wnoCriHMpbaZiHRhohjqdfyeY/S1mZQlaPVCJZQaBuGC4QNlDcl +Qskfvg8o2AqLmmHFMe+Lc638dvCaTWzmybftcsqwrgwAwKVkg1pmemFZ/55c +-----END CERTIFICATE----- diff --git a/tests/certs/oadr310/vtn/key.pem b/tests/certs/oadr310/vtn/key.pem new file mode 100644 index 0000000..d3ca6eb --- /dev/null +++ b/tests/certs/oadr310/vtn/key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQDNt5uZzG/FOzJh +5VKMzSEhkB/zh/9CsK+5hTczQY0tUxS81GnhUaZWL14Jr+bPRr3FifJe6M/cuVLY +7hI3rpPIxPfE/0XrFJOYi/4rYHQZl5NcIKOJV01aWb6uVY6L4j1OHhcKkwINRMg9 +rWbEncpZqX/tLCgVuEaRiK6pUfqCMSq+2ZM1qaw9fFz45rIykBvFlzU9BxgCzptg +6Bfq6obqJlvRr274MwSu1LS3cBr0xOBm1XdmzEF6uWs6rZ8KYnPDo0NAeMDD7fB+ +663ThbQEOGZW5Eti1Of8cYM1ZaHYeKnqlGqkCnV4Q5/qElk5BJWau28emdCgwxCk +/Yncy4wctPEduHnH3eyCRhG10bUaucFm7EEQMjKPaY6CZ/xtm6tcjZ3cXLKpGCK4 +ooV+f2JcCsim33KIZAjI4dVg+fJFJArkwtqvfFYrRB5pPlKdYwEC64q97gv6/aVi +v3hBFeowQe5gcJp9mEn/6uNvirKze0xhLWQO7GjbNxyFTtKuTi7KudM1dKclrKlN +WyqXh8a0SaAle+2aNlLcQubpLBPfz82qc5H6PO16ZO2LXqLfvmYgu96C/kxQZEBu +IvhKj42sGSfsByx8+hFh+d9h0kVa0P6nk/abAH4ol8ZHxnVSJVCEStmjQsjV4/9y +mvVw2Pza9aFsLRWcuRegMWk3B9PeoQIDAQABAoICAF2GYvx6IDp33kD5hB01SSG8 +J3qNUrScJVffw6fO3oNDqULsAzMH7upx4Hvg97s2eyJqw8D4cT/1FUBAhBHCkbeO +LOLn936XvT52LtjDoiTcb4dL422WPjmcYX9asFLSSXLRZUJOcXlc0viPdBBheFRp +JhrKn3K4c4TIsjeSaKR1++wGeNHF0OOnPZF/OHaaY7XiN7cGGZl1Gtjky7Lmau3b +AJWE/5thfniJhZt8IRU0yJyX4g/fPsmvsJtdwddzyROhjvqpq2GoNFVPipiGyb+9 +cHGCmu9Uwh9X8AH/saP7v6xBS3ZGtv81J2fAbcXpWqadHji4T0w3QsLv7ZRvCcRV +a70rhnriPq/VP71fMq3KCfYqlPbL/qXCrvn+pMl0qmbtixnyNIbhERQy8VMJljlB +YYg2D4NHmpRDzCYeCr3C28KOtJS7og8O1LPUiXqS/P+avrKv6Hp3HwC1kufuJsQz +KCrZOe6CipnhePfWpR5fDRABSzaRQ1txCYyKJvijZU1l4XVQHMDhUW/zz6e74q4+ +eC19TuCqGEj6MQ0AhB7x82yHOtrCjWNqGJCCTn3uTIU5Bz4Kqi6HIcnZrnz4bcDg +24ydTDSBH/EtzwqvX4thjsPj4TMRb9p6K2U1Um3huJsGJFfSo+b4hgxfH3kvfZfo +Wws0U/KLLr02GHnj+nFNAoIBAQD/4uJPnkEqos5HzkmZ9WrVQaJ9aCvn74IsjBVb +ymp5bWG0rINmnuCaF1JVxFGcafayDWF8MXU8F3g1/s8YTI5zkYvLmOcF+GEEOLYg +L5YN+cNuHmtWdGIE89VPAc3B9gJ/IYIhywwchdv7S85o09qZ1zgYVxHNvwlYDn1k +1/bkDDh80K5f1Mqf/8HLu613nMmT1Q8cbd2O83BpyhYQKHqcI6shg/qb3UyMYjbr +xl+J5J+8xN5yzNagRoRLg7KCozL81Z6GfwzR5PiTHWQLG5GMrgfGrBvandgowQRh +ST++NGTTmhxFsBuJ5Duo7eWS1x/XnO+UmXOTC654JMAOkMJ/AoIBAQDNzwPrfnZT +hk+LTjNN+djEcb5LKdEluYePeDeS8j/z9T48PaQCl4HRU9zV40KZdo3jJIKKceRU +EGKVGZ1Dpj9+zrkdjO8U7zIlQ5RLxm5jxHCUGE5O4FN05513lIkXUoZlqv1qkIR1 +j8+qHBcfIUffDFQVFU6aRxu71gpJt1SyimzRgecv4tXURRxD8DYZHPNIoLQEBl/g +i23awxQ9kJ3MCD969UeBt9g0bYKhzLcVlEgYYe+FGE8uP57w0yk2uihJ4aX8Hvdt +YOpEXKcJp7Rpv80rxLOoNasWkQkQ9L720PQLNWf66HGo3F+RMJpPdotWWyH2q3Gh +R6oGRaRYKI7fAoIBAE8mrzpRiXbWxVgQx1OIRnOQyeJgxrQHsgx1iGXMRwMogoCQ +nFSx8Ge5rw44m8X2QQjyeqUf51OfLuAE4apyg93bt5JSyaNfISNdJ3jtP00zZPA7 +GJi1h1xg0CqQNEU7A6qvgsFRPX3VKiahXEewwH21xPRCeo9Z+Fn0oXUAGK6R2SdC +pa2e9M5pHlkWxKGrLe88iGAS2+XMB4mxqQF2g/aoUdccK/jqizMhbjugMQ1GWuLZ +QrXgBsOtDF8PbbhbIvpyWYbg5eZHHVzkSn3lGTCa5Gzf7/2/tY1lpyUwszbynf2W +PgiQYPRWVN38OHcjdAYN1U3x6X4iruUa+Q2IWaECggEABq+P8AmGrIXzdZIPxpu0 +OAV96hEssy4Iwldkbsih3Tn9UJWsC02CsRspVGVyl2urhlJ8TyUOBtEVcsSHQFhZ +PbRXfSdD6QkwNtewLJoAogsSoSz89kEBVQ3lJqpTncdXiFv6qYiSVQNb+qm48pDx +NcS/woOTrx1aQErewLBM9xPaHyA6M2sKCUBJf8eA6mXvXU+jKXJPkQrWgcHOmYVm +KuelsEfhj8w6INHh6YGt/Ox5PxmHX2p/v//fGPGzKAz6HBRRgRQ8c3gvhBMCdiTV +jo3li2NzdqZvrWb+xWc+AOSe2PJeM19zucmW7S+IA8bVFfQV8V52bYfXdSa0BQ0k +PwKCAQBQl+jihs2RH3vJhfGjmOLf9BcOsGvUuTwqBuzTKr+39siNLZLDfGE05iuB +ozgy1V7eFmh76olvcybnk7/Flts5lwN6IIvXdN4m8Y6vf0vcI644QYLP9P8bubNM +mVn6/fLR007OvV/Qv0iKmaVIBSWhT/7TOHoiOGPuqNSHRuGb8MzBRC6br0oqInYk +o+fJrruBmEKPIUGvwClADILRCjvjRoY8H0nJt8x0mJ2M4Jxx6A5BpWXrlQylny7i +Fxkg3p39HmInvUxvcV/+xCrwzD0Gb8duCdCI6C460u+S7zJ/0VFj92Y5OvV1+/Tr +Z9ZGqHnUh/jGln0zWKakdt9dWQdH +-----END PRIVATE KEY----- diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..8b1eb7f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,373 @@ +"""Fixtures for VEN client and polling-schedule integration tests.""" + +from __future__ import annotations + +from collections.abc import Iterator + +import pytest +from flask import Flask +from flask_login import login_user +from flask_sqlalchemy import SQLAlchemy + +from flexmeasures.data.models.user import User + +from flexmeasures_openadr3.utils.ven_clients import ( + VenClient, + VenClientFormData, + VenClientRepository, + VenSensorConfigFormData, +) +from flexmeasures_openadr3.utils.ven_jobs import VenFetchJobScheduler + +import os +from pathlib import Path +from typing import Iterable +from openadr3_client.oadr310.models.event.event import ExistingEvent +from testcontainers.core.network import Network +from testcontainers.keycloak import KeycloakContainer +from tests.test_container.integration_types import ( + IntegrationTestVTNClient, + IntegrationTestVTNServer, + OAuthConfiguration, +) +from tests.test_container.oadr310_vtn_test_container import ( + OpenAdr310VtnTestContainer, +) +from tests.test_container.openadr_vtn_setup import ( + create_bl_http_client, + seed_capacity_limit_event, +) + + +_TESTS_ROOT = Path(__file__).resolve().parents[0] +_KEYCLOAK_REALM_IMPORT_FILE = _TESTS_ROOT / "keycloak_integration_realm.json" + + +KEYCLOAK_REALM_NAME = "integration-test-realm" + +# Keycloak OAuth test clients (from `tests/keycloak_integration_realm.json`) +KEYCLOAK_BL_CLIENT_ID = "test-client-id" +KEYCLOAK_BL_CLIENT_SECRET = "my-client-secret" +KEYCLOAK_VEN1_CLIENT_ID = "test-ven-1" +KEYCLOAK_VEN2_CLIENT_ID = "test-ven-2" +KEYCLOAK_VEN_CLIENT_SECRET = "my-client-secret" + +KEYCLOAK_INTERNAL_BASE_URL = ( + f"http://keycloak:8080/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect" +) +KEYCLOAK_INTERNAL_TOKEN_URL = f"{KEYCLOAK_INTERNAL_BASE_URL}/token" +KEYCLOAK_INTERNAL_JWKS_URL = f"{KEYCLOAK_INTERNAL_BASE_URL}/certs" + +# Scopes expected by OpenLEADR-rs (used when testing against OpenLEADR-rs VTNs). +# These match the scope strings parsed by OpenLEADR-rs in `openleadr-vtn/src/jwt.rs`. +OPENLEADR_RS_BL_SCOPES = [ + "read_all", + "write_programs", + "write_events", + "write_reports", + "write_subscriptions", + "write_vens", + "write_users", +] +OPENLEADR_RS_VEN_SCOPES = [ + "read_targets", + "read_ven_objects", + "write_reports", + "write_subscriptions", + "write_vens", +] + + +@pytest.fixture +def clean_forecasting_queue(app: Flask) -> Iterator[None]: + """Reset the test Redis DB without queue.empty() (fakeredis lacks EVALSHA).""" + app.redis_connection.flushdb() + yield + app.redis_connection.flushdb() + + +@pytest.fixture +def ven_client_repository() -> VenClientRepository: + """Repository bound to the current app and database session.""" + return VenClientRepository() + + +@pytest.fixture +def ven_fetch_job_scheduler( + ven_client_repository: VenClientRepository, +) -> VenFetchJobScheduler: + """RQ job scheduler for daily fetch-events jobs.""" + return VenFetchJobScheduler(ven_client_repository) + + +@pytest.fixture +def sample_ven_client_form_data() -> VenClientFormData: + """Valid VEN client connection fields for create/update tests.""" + return VenClientFormData( + name="test-ven-client", + vtn_url="https://vtn.example/openadr3", + oauth_client_id="test-client-id", + oauth_client_secret="test-client-secret", + oauth_token_url="https://vtn.example/oauth/token", + scopes=["read", "write"], + ) + + +@pytest.fixture +def sample_sensor_config_form_data() -> VenSensorConfigFormData: + """Valid polling schedule fields for create/update tests.""" + return VenSensorConfigFormData( + name="daily-poll", + targets=["target-a", "target-b"], + utc_trigger_time="02:30:00", + fetch_import_capacity_limits=True, + fetch_export_capacity_limits=True, + ) + + +@pytest.fixture +def logged_in_prosumer( + app: Flask, + setup_roles_users_fresh_db: dict[str, int], + db: SQLAlchemy, +) -> Iterator[User]: + """Prosumer user with an active Flask-Login session for repository calls.""" + user = db.session.get(User, setup_roles_users_fresh_db["Test Prosumer User"]) + assert user is not None + + with app.test_request_context(): + login_user(user) + yield user + + +@pytest.fixture +def created_ven_client( + fresh_db: object, + db: SQLAlchemy, + logged_in_prosumer: User, + ven_client_repository: VenClientRepository, + sample_ven_client_form_data: VenClientFormData, +) -> Iterator[VenClient]: + """VEN client persisted in the database.""" + ven_client = ven_client_repository.create(sample_ven_client_form_data) + db.session.commit() + yield ven_client + + +@pytest.fixture +def created_ven_client_with_schedule( + db: SQLAlchemy, + created_ven_client: VenClient, + ven_client_repository: VenClientRepository, + sample_sensor_config_form_data: VenSensorConfigFormData, +) -> Iterator[VenClient]: + """VEN client with one polling schedule and linked sensors.""" + sensor_config = ven_client_repository.append_sensor_config( + created_ven_client, + sample_sensor_config_form_data, + ) + db.session.commit() + assert sensor_config is not None + yield created_ven_client + + +@pytest.fixture(scope="session") +def integration_test_docker_network() -> Iterator[Network]: + """Shared Docker network for Keycloak and OpenLEADR-rs VTN testcontainers.""" + network = Network() + network.create() + yield network + network.remove() + + +@pytest.fixture(scope="session", autouse=True) +def _allow_http_oauth_for_test_keycloak() -> Iterator[None]: + """Keycloak testcontainer serves HTTP; oauthlib requires this in non-production tests.""" + previous_insecure = os.environ.get("OAUTHLIB_INSECURE_TRANSPORT") + previous_scope = os.environ.get("OAUTHLIB_RELAX_TOKEN_SCOPE") + os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" + os.environ["OAUTHLIB_RELAX_TOKEN_SCOPE"] = "1" + yield + if previous_insecure is None: + os.environ.pop("OAUTHLIB_INSECURE_TRANSPORT", None) + else: + os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = previous_insecure + if previous_scope is None: + os.environ.pop("OAUTHLIB_RELAX_TOKEN_SCOPE", None) + else: + os.environ["OAUTHLIB_RELAX_TOKEN_SCOPE"] = previous_scope + + +@pytest.fixture(scope="session") +def integration_test_auth_server( + integration_test_docker_network: Network, +) -> Iterator[KeycloakContainer]: + """Keycloak with the integration-test realm (OAuth for BL and VEN clients).""" + keycloak = ( + KeycloakContainer("quay.io/keycloak/keycloak:25.0.4") + .with_network(integration_test_docker_network) + .with_network_aliases("keycloak") + .with_realm_import_file(str(_KEYCLOAK_REALM_IMPORT_FILE)) + ) + with keycloak: + yield keycloak + + +@pytest.fixture(scope="session") +def integration_test_oauth_client_bl_client( + integration_test_auth_server: KeycloakContainer, +) -> OAuthConfiguration: + """ + A testcontainers keycloak fixture which is initialized once per test run. + + Yields an OAuthConfiguration which contains an oauth client that was created + for the scope of this test session. This OAUTH client is configured to have the BL scopes inside an OpenADR VTN. + + Yields: + IntegrationTestOAuthClient: The integration test oauth client. + + """ + token_url = ( + integration_test_auth_server.get_url() + + f"/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect/token" + ) + return OAuthConfiguration( + client_id=KEYCLOAK_BL_CLIENT_ID, + client_secret=KEYCLOAK_BL_CLIENT_SECRET, + token_url=token_url, + scopes=None, + audience=None, + ) + + +@pytest.fixture(scope="session") +def integration_test_openadr310_vtn_server( + integration_test_docker_network: Network, +) -> Iterable[IntegrationTestVTNServer]: + """ + A testcontainers openleadr-vtn (OpenADR 3.1.0) fixture which is initialized once per test run. + + Yields an OpenAdr310VtnTestContainer which contains the base URL of the VTN being hosted. + + Args: + integration_test_docker_network (Network): The docker network to which the keycloak container will be connected. + integration_test_oauth_client (OAuthConfiguration): OAuth client for retrieving access tokens (Keycloak). + + Yields: + Iterable[OpenAdr310VtnTestContainer]: The integration test vtn client. + + """ + with OpenAdr310VtnTestContainer( + oauth_jwks_url=KEYCLOAK_INTERNAL_JWKS_URL, + oauth_valid_audiences="https://integration.test.elaad.nl,", + oauth_token_url=KEYCLOAK_INTERNAL_TOKEN_URL, + network=integration_test_docker_network, + ) as vtn_container: + yield IntegrationTestVTNServer( + vtn_base_url=vtn_container.get_base_url(), + allow_insecure_http=True, + ) + + +@pytest.fixture(scope="session") +def bl_oauth_configuration( + integration_test_auth_server: KeycloakContainer, +) -> OAuthConfiguration: + """ + A testcontainers keycloak fixture which is initialized once per test run. + + Yields an OAuthConfiguration which contains an oauth client that was created + for the scope of this test session. This OAUTH client is configured to have the BL scopes inside an OpenADR VTN. + """ + token_url = ( + integration_test_auth_server.get_url() + + f"/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect/token" + ) + return OAuthConfiguration( + client_id=KEYCLOAK_BL_CLIENT_ID, + client_secret=KEYCLOAK_BL_CLIENT_SECRET, + token_url=token_url, + scopes=None, + audience=None, + ) + + +@pytest.fixture(scope="session") +def ven_oauth_configuration( + integration_test_auth_server: KeycloakContainer, +) -> OAuthConfiguration: + """ + A testcontainers keycloak fixture which is initialized once per test run. + + Yields an OAuthConfiguration which contains an oauth client that was created + for the scope of this test session. This OAUTH client is configured to have the VEN scopes inside an OpenADR VTN. + """ + token_url = ( + integration_test_auth_server.get_url() + + f"/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect/token" + ) + return OAuthConfiguration( + client_id=KEYCLOAK_VEN1_CLIENT_ID, + client_secret=KEYCLOAK_VEN_CLIENT_SECRET, + token_url=token_url, + scopes=None, + audience=None, + ) + + +@pytest.fixture(scope="session") +def bl_client( + integration_test_openadr310_vtn_server: IntegrationTestVTNServer, + bl_oauth_configuration: OAuthConfiguration, +) -> IntegrationTestVTNClient: + """ + Returns an IntegrationTestVTNClient which contains VTN server information and an OAUTH configuration which can be used + to fetch a BL access token to communicate with the VTN. + + Args: + integration_test_openadr310_vtn_server (IntegrationTestVTNServer): OpenLEADR-rs VTN server. + bl_oauth_configuration (OAuthConfiguration): OAuth configuration for the BL token. + + Yields: + Iterable[IntegrationTestVTNClient]: The integration test vtn client. + + """ + return IntegrationTestVTNClient( + vtn_configuration=integration_test_openadr310_vtn_server, + oauth_configuration=bl_oauth_configuration, + ) + + +@pytest.fixture(scope="session") +def ven_client( + integration_test_openadr310_vtn_server: IntegrationTestVTNServer, + ven_oauth_configuration: OAuthConfiguration, +) -> IntegrationTestVTNClient: + """ + Returns an IntegrationTestVTNClient which contains VTN server information and an OAUTH configuration which can be used + to fetch a VEN access token to communicate with the VTN. + + Args: + integration_test_openadr310_vtn_server (IntegrationTestVTNServer): OpenLEADR-rs VTN server. + ven_oauth_configuration (OAuthConfiguration): OAuth configuration for the VEN token. + + Yields: + Iterable[IntegrationTestVTNClient]: The integration test vtn client. + + """ + return IntegrationTestVTNClient( + vtn_configuration=integration_test_openadr310_vtn_server, + oauth_configuration=ven_oauth_configuration, + ) + + +@pytest.fixture(scope="session") +def vtn_capacity_limit_event_seed( + bl_client: IntegrationTestVTNClient, +) -> ExistingEvent: + """Program + 24h capacity-limit event on the VTN, starting one hour from seed time.""" + bl_http = create_bl_http_client(bl_client) + seeded_event = seed_capacity_limit_event( + bl_http, + ) + return seeded_event diff --git a/tests/keycloak_integration_realm.json b/tests/keycloak_integration_realm.json new file mode 100644 index 0000000..3e33860 --- /dev/null +++ b/tests/keycloak_integration_realm.json @@ -0,0 +1,239 @@ +{ + "realm": "integration-test-realm", + "enabled": true, + "clientScopes": [ + { + "name": "test/BL", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + } + }, + { + "name": "test/VEN", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + } + }, + { + "name": "read_all", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + } + }, + { + "name": "read_targets", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + } + }, + { + "name": "read_ven_objects", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + } + }, + { + "name": "write_programs", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + } + }, + { + "name": "write_events", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + } + }, + { + "name": "write_reports", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + } + }, + { + "name": "write_subscriptions", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + } + }, + { + "name": "write_vens", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + } + }, + { + "name": "write_users", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + } + } + ], + "clients": [ + { + "clientId": "test-client-id", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "my-client-secret", + "protocol": "openid-connect", + "publicClient": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "defaultClientScopes": [ + "profile", + "email", + "test/BL", + "test/VEN", + "read_all", + "read_targets", + "read_ven_objects", + "write_programs", + "write_events", + "write_reports", + "write_subscriptions", + "write_vens", + "write_users" + ], + "protocolMappers": [ + { + "name": "audience", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-mapper", + "consentRequired": false, + "config": { + "included.client.audience": "https://integration.test.elaad.nl", + "id.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "name": "nbf mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-hardcoded-claim-mapper", + "config": { + "claim.value": "1745926210", + "claim.name": "nbf", + "jsonType.label": "long", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "clientId": "test-ven-1", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "my-client-secret", + "protocol": "openid-connect", + "publicClient": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "defaultClientScopes": [ + "profile", + "email", + "test/VEN", + "read_targets", + "read_ven_objects", + "write_reports", + "write_subscriptions", + "write_vens" + ], + "protocolMappers": [ + { + "name": "audience", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-mapper", + "consentRequired": false, + "config": { + "included.client.audience": "https://integration.test.elaad.nl", + "id.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "name": "nbf mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-hardcoded-claim-mapper", + "config": { + "claim.value": "1745926210", + "claim.name": "nbf", + "jsonType.label": "long", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "clientId": "test-ven-2", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "my-client-secret", + "protocol": "openid-connect", + "publicClient": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "defaultClientScopes": [ + "profile", + "email", + "test/VEN", + "read_targets", + "read_ven_objects", + "write_reports", + "write_subscriptions", + "write_vens" + ], + "protocolMappers": [ + { + "name": "audience", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-mapper", + "consentRequired": false, + "config": { + "included.client.audience": "https://integration.test.elaad.nl", + "id.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "name": "nbf mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-hardcoded-claim-mapper", + "config": { + "claim.value": "1745926210", + "claim.name": "nbf", + "jsonType.label": "long", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true" + } + } + ] + } + ] +} diff --git a/tests/test_container/__init__.py b/tests/test_container/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_container/conftest.py b/tests/test_container/conftest.py new file mode 100644 index 0000000..1011b43 --- /dev/null +++ b/tests/test_container/conftest.py @@ -0,0 +1,262 @@ +import os +from collections.abc import Iterator +from pathlib import Path +from typing import Iterable +from openadr3_client.oadr310.models.event.event import ExistingEvent +import pytest + +from testcontainers.core.network import Network +from testcontainers.keycloak import KeycloakContainer + +from tests.test_container.integration_types import ( + IntegrationTestVTNClient, + IntegrationTestVTNServer, + OAuthConfiguration, +) +from tests.test_container.oadr310_vtn_test_container import ( + OpenAdr310VtnTestContainer, +) +from tests.test_container.openadr_vtn_setup import ( + create_bl_http_client, + seed_capacity_limit_event, +) + +_TESTS_ROOT = Path(__file__).resolve().parents[1] +_KEYCLOAK_REALM_IMPORT_FILE = _TESTS_ROOT / "keycloak_integration_realm.json" + + +KEYCLOAK_REALM_NAME = "integration-test-realm" + +# Keycloak OAuth test clients (from `tests/keycloak_integration_realm.json`) +KEYCLOAK_BL_CLIENT_ID = "test-client-id" +KEYCLOAK_BL_CLIENT_SECRET = "my-client-secret" +KEYCLOAK_VEN1_CLIENT_ID = "test-ven-1" +KEYCLOAK_VEN2_CLIENT_ID = "test-ven-2" +KEYCLOAK_VEN_CLIENT_SECRET = "my-client-secret" + +KEYCLOAK_INTERNAL_BASE_URL = ( + f"http://keycloak:8080/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect" +) +KEYCLOAK_INTERNAL_TOKEN_URL = f"{KEYCLOAK_INTERNAL_BASE_URL}/token" +KEYCLOAK_INTERNAL_JWKS_URL = f"{KEYCLOAK_INTERNAL_BASE_URL}/certs" + +# Scopes expected by OpenLEADR-rs (used when testing against OpenLEADR-rs VTNs). +# These match the scope strings parsed by OpenLEADR-rs in `openleadr-vtn/src/jwt.rs`. +OPENLEADR_RS_BL_SCOPES = [ + "read_all", + "write_programs", + "write_events", + "write_reports", + "write_subscriptions", + "write_vens", + "write_users", +] +OPENLEADR_RS_VEN_SCOPES = [ + "read_targets", + "read_ven_objects", + "write_reports", + "write_subscriptions", + "write_vens", +] + + +@pytest.fixture(scope="session") +def integration_test_docker_network() -> Iterator[Network]: + """Shared Docker network for Keycloak and OpenLEADR-rs VTN testcontainers.""" + network = Network() + network.create() + yield network + network.remove() + + +@pytest.fixture(scope="session", autouse=True) +def _allow_http_oauth_for_test_keycloak() -> Iterator[None]: + """Keycloak testcontainer serves HTTP; oauthlib requires this in non-production tests.""" + previous_insecure = os.environ.get("OAUTHLIB_INSECURE_TRANSPORT") + previous_scope = os.environ.get("OAUTHLIB_RELAX_TOKEN_SCOPE") + os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" + os.environ["OAUTHLIB_RELAX_TOKEN_SCOPE"] = "1" + yield + if previous_insecure is None: + os.environ.pop("OAUTHLIB_INSECURE_TRANSPORT", None) + else: + os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = previous_insecure + if previous_scope is None: + os.environ.pop("OAUTHLIB_RELAX_TOKEN_SCOPE", None) + else: + os.environ["OAUTHLIB_RELAX_TOKEN_SCOPE"] = previous_scope + + +@pytest.fixture(scope="session") +def integration_test_auth_server( + integration_test_docker_network: Network, +) -> Iterator[KeycloakContainer]: + """Keycloak with the integration-test realm (OAuth for BL and VEN clients).""" + keycloak = ( + KeycloakContainer("quay.io/keycloak/keycloak:25.0.4") + .with_network(integration_test_docker_network) + .with_network_aliases("keycloak") + .with_realm_import_file(str(_KEYCLOAK_REALM_IMPORT_FILE)) + ) + with keycloak: + yield keycloak + + +@pytest.fixture(scope="session") +def integration_test_oauth_client_bl_client( + integration_test_auth_server: KeycloakContainer, +) -> OAuthConfiguration: + """ + A testcontainers keycloak fixture which is initialized once per test run. + + Yields an OAuthConfiguration which contains an oauth client that was created + for the scope of this test session. This OAUTH client is configured to have the BL scopes inside an OpenADR VTN. + + Yields: + IntegrationTestOAuthClient: The integration test oauth client. + + """ + token_url = ( + integration_test_auth_server.get_url() + + f"/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect/token" + ) + return OAuthConfiguration( + client_id=KEYCLOAK_BL_CLIENT_ID, + client_secret=KEYCLOAK_BL_CLIENT_SECRET, + token_url=token_url, + scopes=None, + audience=None, + ) + + +@pytest.fixture(scope="session") +def integration_test_openadr310_vtn_server( + integration_test_docker_network: Network, +) -> Iterable[IntegrationTestVTNServer]: + """ + A testcontainers openleadr-vtn (OpenADR 3.1.0) fixture which is initialized once per test run. + + Yields an OpenAdr310VtnTestContainer which contains the base URL of the VTN being hosted. + + Args: + integration_test_docker_network (Network): The docker network to which the keycloak container will be connected. + integration_test_oauth_client (OAuthConfiguration): OAuth client for retrieving access tokens (Keycloak). + + Yields: + Iterable[OpenAdr310VtnTestContainer]: The integration test vtn client. + + """ + with OpenAdr310VtnTestContainer( + oauth_jwks_url=KEYCLOAK_INTERNAL_JWKS_URL, + oauth_valid_audiences="https://integration.test.elaad.nl,", + oauth_token_url=KEYCLOAK_INTERNAL_TOKEN_URL, + network=integration_test_docker_network, + ) as vtn_container: + yield IntegrationTestVTNServer( + vtn_base_url=vtn_container.get_base_url(), + allow_insecure_http=True, + ) + + +@pytest.fixture(scope="session") +def bl_oauth_configuration( + integration_test_auth_server: KeycloakContainer, +) -> OAuthConfiguration: + """ + A testcontainers keycloak fixture which is initialized once per test run. + + Yields an OAuthConfiguration which contains an oauth client that was created + for the scope of this test session. This OAUTH client is configured to have the BL scopes inside an OpenADR VTN. + """ + token_url = ( + integration_test_auth_server.get_url() + + f"/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect/token" + ) + return OAuthConfiguration( + client_id=KEYCLOAK_BL_CLIENT_ID, + client_secret=KEYCLOAK_BL_CLIENT_SECRET, + token_url=token_url, + scopes=None, + audience=None, + ) + + +@pytest.fixture(scope="session") +def ven_oauth_configuration( + integration_test_auth_server: KeycloakContainer, +) -> OAuthConfiguration: + """ + A testcontainers keycloak fixture which is initialized once per test run. + + Yields an OAuthConfiguration which contains an oauth client that was created + for the scope of this test session. This OAUTH client is configured to have the VEN scopes inside an OpenADR VTN. + """ + token_url = ( + integration_test_auth_server.get_url() + + f"/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect/token" + ) + return OAuthConfiguration( + client_id=KEYCLOAK_VEN1_CLIENT_ID, + client_secret=KEYCLOAK_VEN_CLIENT_SECRET, + token_url=token_url, + scopes=None, + audience=None, + ) + + +@pytest.fixture(scope="session") +def bl_client( + integration_test_openadr310_vtn_server: IntegrationTestVTNServer, + bl_oauth_configuration: OAuthConfiguration, +) -> IntegrationTestVTNClient: + """ + Returns an IntegrationTestVTNClient which contains VTN server information and an OAUTH configuration which can be used + to fetch a BL access token to communicate with the VTN. + + Args: + integration_test_openadr310_vtn_server (IntegrationTestVTNServer): OpenLEADR-rs VTN server. + bl_oauth_configuration (OAuthConfiguration): OAuth configuration for the BL token. + + Yields: + Iterable[IntegrationTestVTNClient]: The integration test vtn client. + + """ + return IntegrationTestVTNClient( + vtn_configuration=integration_test_openadr310_vtn_server, + oauth_configuration=bl_oauth_configuration, + ) + + +@pytest.fixture(scope="session") +def ven_client( + integration_test_openadr310_vtn_server: IntegrationTestVTNServer, + ven_oauth_configuration: OAuthConfiguration, +) -> IntegrationTestVTNClient: + """ + Returns an IntegrationTestVTNClient which contains VTN server information and an OAUTH configuration which can be used + to fetch a VEN access token to communicate with the VTN. + + Args: + integration_test_openadr310_vtn_server (IntegrationTestVTNServer): OpenLEADR-rs VTN server. + ven_oauth_configuration (OAuthConfiguration): OAuth configuration for the VEN token. + + Yields: + Iterable[IntegrationTestVTNClient]: The integration test vtn client. + + """ + return IntegrationTestVTNClient( + vtn_configuration=integration_test_openadr310_vtn_server, + oauth_configuration=ven_oauth_configuration, + ) + + +@pytest.fixture(scope="session") +def vtn_capacity_limit_event_seed( + bl_client: IntegrationTestVTNClient, +) -> ExistingEvent: + """Program + 24h capacity-limit event on the VTN, starting one hour from seed time.""" + bl_http = create_bl_http_client(bl_client) + seeded_event = seed_capacity_limit_event( + bl_http, + ) + return seeded_event diff --git a/tests/test_container/flexmeasures_test_postgres_container.py b/tests/test_container/flexmeasures_test_postgres_container.py new file mode 100644 index 0000000..ca1d453 --- /dev/null +++ b/tests/test_container/flexmeasures_test_postgres_container.py @@ -0,0 +1,75 @@ +"""PostgreSQL testcontainer used by flexmeasures during integration tests.""" + +from __future__ import annotations + +from dataclasses import dataclass +from types import TracebackType +from typing import Self + +from testcontainers.postgres import PostgresContainer + +FLEXMEASURES_TEST_POSTGRES_IMAGE = "postgres" +FLEXMEASURES_TEST_DB_NAME = "flexmeasures_test" +FLEXMEASURES_TEST_DB_USER = "flexmeasures_test" +FLEXMEASURES_TEST_DB_PASSWORD = "flexmeasures_test" # noqa: S105 + + +@dataclass(frozen=True) +class FlexMeasuresTestPostgres: + """Connection details for the FlexMeasures test database.""" + + database_uri: str + host: str + port: int + dbname: str + username: str + password: str + + +class FlexMeasuresTestPostgresContainer: + """Session-scoped PostgreSQL container for FlexMeasures plugin tests.""" + + def __init__(self) -> None: + container = PostgresContainer( + image=FLEXMEASURES_TEST_POSTGRES_IMAGE, + username=FLEXMEASURES_TEST_DB_USER, + password=FLEXMEASURES_TEST_DB_PASSWORD, + dbname=FLEXMEASURES_TEST_DB_NAME, + ) + self._container = container + + def start(self) -> Self: + self._container.start() + return self + + def stop(self) -> None: + self._container.stop() + + def _get_database_uri(self) -> str: + """SQLAlchemy URI for FlexMeasures ``TestingConfig`` (localhost + mapped port).""" + url = str(self._container.get_connection_url()) + # Align with flexmeasures.utils.config_defaults.TestingConfig (postgresql://). + return url.replace("postgresql+psycopg2://", "postgresql://", 1) + + def get_connection_info(self) -> FlexMeasuresTestPostgres: + host = self._container.get_container_host_ip() + port = int(self._container.get_exposed_port(self._container.port)) + return FlexMeasuresTestPostgres( + database_uri=self._get_database_uri(), + host=host, + port=port, + dbname=FLEXMEASURES_TEST_DB_NAME, + username=FLEXMEASURES_TEST_DB_USER, + password=FLEXMEASURES_TEST_DB_PASSWORD, + ) + + def __enter__(self) -> Self: + return self.start() + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.stop() diff --git a/tests/test_container/integration_types.py b/tests/test_container/integration_types.py new file mode 100644 index 0000000..c484510 --- /dev/null +++ b/tests/test_container/integration_types.py @@ -0,0 +1,26 @@ +"""Shared types for OpenADR integration and E2E tests.""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class OAuthConfiguration: + client_id: str + client_secret: str + token_url: str + scopes: list[str] | None + audience: str | None + + +@dataclass +class IntegrationTestVTNServer: + vtn_base_url: str + allow_insecure_http: bool + + +@dataclass +class IntegrationTestVTNClient: + vtn_configuration: IntegrationTestVTNServer + oauth_configuration: OAuthConfiguration diff --git a/tests/test_container/oadr310_vtn_test_container.py b/tests/test_container/oadr310_vtn_test_container.py new file mode 100644 index 0000000..a9317e2 --- /dev/null +++ b/tests/test_container/oadr310_vtn_test_container.py @@ -0,0 +1,225 @@ +# SPDX-FileCopyrightText: Contributors to openadr3-client +# +# SPDX-License-Identifier: Apache-2.0 + +import re +import threading +from collections import deque +from types import TracebackType +from typing import Any, Self +import uuid + +import docker.errors +from testcontainers.core.container import DockerContainer, LogMessageWaitStrategy +from testcontainers.core.network import Network +from testcontainers.postgres import PostgresContainer + +_LOG_BUFFER_MAX_LINES = 5_000 + + +class OpenLeadrVtnTestContainer: + """A test container for an OpenADR 3.1.0 VTN.""" + + def __init__( + self, + oauth_jwks_url: str, + oauth_valid_audiences: str, + oauth_token_url: str, + network: Network | None = None, + oauth_key_type: str = "RSA", + postgres_image: str = "postgres:16", + vtn_port: int = 3000, + postgres_port: int = 5432, + postgres_user: str = "openadr", + postgres_password: str = "openadr", # noqa: S107 + postgres_db: str = "openadr", + openleadr_rs_image: str = "ghcr.io/openleadr/openleadr-rs:1779099254-6c5a5cc", + **kwargs: dict[str, Any], + ) -> None: + """ + Initialize the VTN test container. + + Args: + oauth_jwks_url (str): The OAuth server JWKS URL. Used by OpenLEADR-rs to validate JWT signatures. + oauth_valid_audiences (str): The valid audiences for the OAuth token, the provided value must be a + comma seperated list of valid audiences. + oauth_token_url (str): The OAuth token endpoint URL. Required by OpenLEADR-rs, also when `OAUTH_TYPE=EXTERNAL`. + network (Network | None, optional): The Docker network to use. If None, a new network will be created. + oauth_key_type (str, optional): The type of OAuth key. Defaults to "RSA". + openleadr_rs_image (str, optional): The image to use for the VTN. + Defaults to "ghcr.io/openleadr/openleadr-rs:1779099254-6c5a5cc". + postgres_image (str, optional): The image to use for the PostgreSQL container. Defaults to "postgres:16". + vtn_port (int, optional): The port on which the VTN will listen. Defaults to 3000. + postgres_port (int, optional): The port for the PostgreSQL container. Defaults to 5432. + postgres_user (str, optional): PostgreSQL username. Defaults to "openadr". + postgres_password (str, optional): PostgreSQL password. Defaults to "openadr". + postgres_db (str, optional): PostgreSQL database name. Defaults to "openadr". + **kwargs: Additional arguments to pass to the DockerContainer constructor. + + """ + self._vtn_port = vtn_port + self._postgres_port = postgres_port + self._log_stop_event = threading.Event() + self._vtn_log_lines: deque[str] = deque(maxlen=_LOG_BUFFER_MAX_LINES) + self._vtn_log_thread: threading.Thread | None = None + # Unique alias so multiple VTN instances on the same Docker network don't + # share a "vtndb" DNS entry and accidentally resolve to each other's Postgres. + self._postgres_alias = f"vtndb-{uuid.uuid4().hex[:8]}" + + if network is None: + self._internal_network = True + self._network = Network() + else: + self._internal_network = False + self._network = network + + # Initialize PostgreSQL container + self._postgres = ( + PostgresContainer( + image=postgres_image, + port=self._postgres_port, + username=postgres_user, + password=postgres_password, + dbname=postgres_db, + ) + .with_network(self._network) + .with_network_aliases(self._postgres_alias) + .waiting_for( + LogMessageWaitStrategy("database system is ready to accept connections") + ) + ) + + # Initialize VTN container with the static environment variables. + self._vtn = ( + DockerContainer(openleadr_rs_image, **kwargs) + .with_kwargs(platform="linux/amd64") + .with_network(self._network) + .with_exposed_ports(self._vtn_port) + .with_env(key="OAUTH_TYPE", value="EXTERNAL") + .with_env(key="OAUTH_VALID_AUDIENCES", value=oauth_valid_audiences) + .with_env(key="OAUTH_KEY_TYPE", value=oauth_key_type) + .with_env(key="OAUTH_JWKS_LOCATION", value=oauth_jwks_url) + .with_env(key="OAUTH_TOKEN_URL", value=oauth_token_url) + .with_env(key="PG_PORT", value=self._postgres.port) + .with_env(key="PG_DB", value=postgres_db) + .with_env(key="PG_USER", value=postgres_user) + .with_env(key="PG_PASSWORD", value=postgres_password) + .with_env(key="PG_TZ", value="Europe/Amsterdam") + .with_env(key="RUST_BACKTRACE", value="full") + .with_env(key="RUST_LOG", value="trace") + .waiting_for( + LogMessageWaitStrategy( + re.compile(r".*ListStore\.__init__\(\).*"), re.DOTALL + ) + ) + ) + + def _start_vtn_log_capture(self) -> None: + """Capture VTN logs into an in-memory ring buffer.""" + if self._vtn_log_thread is not None: + return + + wrapped = self._vtn.get_wrapped_container() + + def _run() -> None: + try: + for chunk in wrapped.logs( + stream=True, follow=True, stdout=True, stderr=True + ): + if self._log_stop_event.is_set(): + break + chunk_bytes = ( + chunk + if isinstance(chunk, bytes | bytearray) + else str(chunk).encode("utf-8", errors="replace") + ) + text = chunk_bytes.decode("utf-8", errors="replace") + for line in text.splitlines(): + if self._log_stop_event.is_set(): + break + self._vtn_log_lines.append(line) + except (docker.errors.DockerException, OSError): + return + + self._vtn_log_thread = threading.Thread( + target=_run, name="oadr310-ref-vtn-logs", daemon=True + ) + self._vtn_log_thread.start() + + def get_vtn_log_tail(self, max_lines: int = 500) -> list[str]: + """Return the last N captured VTN log lines (best-effort).""" + if max_lines <= 0: + return [] + if max_lines >= len(self._vtn_log_lines): + return list(self._vtn_log_lines) + return list(self._vtn_log_lines)[-max_lines:] + + def start(self) -> Self: + """Start both containers and wait for them to be ready.""" + if self._internal_network: + # Internal network, so we must create the network manually. + self._network.create() + + self._postgres.start() + + # Create the database URL for the VTN. Which can only be determined + # after the postgres container has started. + vtn_db_url = ( + self._postgres.get_connection_url(driver=None) + .replace("postgresql", "postgres") + .replace("localhost", self._postgres_alias) + .replace(str(self._postgres.get_exposed_port(self._postgres_port)), "5432") + ) + + # Configure the VTN with the MQTT broker URL prior to starting it. + self._vtn.with_env("DATABASE_URL", value=vtn_db_url).waiting_for( + LogMessageWaitStrategy("pg_advisory_unlock") + ).start() + self._start_vtn_log_capture() + return self + + def get_base_url(self) -> str: + """Get the base URL for the VTN.""" + return f"http://localhost:{self._vtn.get_exposed_port(self._vtn_port)}" + + def stop(self) -> None: + """Stop the openleadr test container and its dependencies.""" + self._log_stop_event.set() + self._vtn.stop() + self._postgres.stop() + if self._internal_network: + # Internal network, so we must remove the network ourselves. + self._network.remove() + + def __enter__(self) -> Self: + """Context manager entry.""" + return self.start() + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + """Context manager exit.""" + self.stop() + + +class OpenAdr310VtnTestContainer(OpenLeadrVtnTestContainer): + """A test container for an OpenADR 3.1.0 VTN.""" + + def __init__( + self, + oauth_jwks_url: str, + oauth_valid_audiences: str, + oauth_token_url: str, + version: str = "1779099254-6c5a5cc", + network: Network | None = None, + ) -> None: + super().__init__( + openleadr_rs_image=f"ghcr.io/openleadr/openleadr-rs:{version}", + oauth_jwks_url=oauth_jwks_url, + oauth_valid_audiences=oauth_valid_audiences, + oauth_token_url=oauth_token_url, + network=network, + ) diff --git a/tests/test_container/openadr_vtn_setup.py b/tests/test_container/openadr_vtn_setup.py new file mode 100644 index 0000000..073aa6a --- /dev/null +++ b/tests/test_container/openadr_vtn_setup.py @@ -0,0 +1,127 @@ +"""Helpers to seed OpenADR 3.1 VTN test data for end-to-end tests.""" + +from __future__ import annotations + +from datetime import datetime, timedelta, timezone +import random +from typing import Tuple +import uuid + +from openadr3_client.bl.http_factory import BusinessLogicHttpClientFactory +from openadr3_client.oadr310._bl.client import BusinessLogicClient +from openadr3_client.oadr310._ven.client import VirtualEndNodeClient +from openadr3_client.oadr310.models.event.event import ExistingEvent, Interval, NewEvent +from openadr3_client.oadr310.models.event.event_payload import ( + EventPayload, + EventPayloadDescriptor, + EventPayloadType, +) +from openadr3_client.oadr310.models.program.program import NewProgram +from openadr3_client.oadr310.models.unit import Unit +from openadr3_client.ven.http_factory import VirtualEndNodeHttpClientFactory +from openadr3_client._models.common.interval_period import IntervalPeriod +from openadr3_client.version import OADRVersion + +from tests.test_container.integration_types import IntegrationTestVTNClient + + +def create_bl_http_client(vtn_client: IntegrationTestVTNClient) -> BusinessLogicClient: + """Build a Business Logic HTTP client from integration-test VTN/OAuth settings.""" + oauth = vtn_client.oauth_configuration + vtn = vtn_client.vtn_configuration + return BusinessLogicHttpClientFactory.create_http_bl_client( + vtn_base_url=vtn.vtn_base_url, + client_id=oauth.client_id, + client_secret=oauth.client_secret, + token_url=oauth.token_url, + scopes=oauth.scopes, + audience=oauth.audience, + verify_vtn_tls_certificate=False, + allow_insecure_http=vtn.allow_insecure_http, + version=OADRVersion.OADR_310, + ) # type: ignore[return-value] + + +def create_ven_http_client( + vtn_client: IntegrationTestVTNClient, +) -> VirtualEndNodeClient: + """Build a VEN HTTP client from integration-test VTN/OAuth settings.""" + oauth = vtn_client.oauth_configuration + vtn = vtn_client.vtn_configuration + return VirtualEndNodeHttpClientFactory.create_http_ven_client( + vtn_base_url=vtn.vtn_base_url, + client_id=oauth.client_id, + client_secret=oauth.client_secret, + token_url=oauth.token_url, + scopes=oauth.scopes, + verify_vtn_tls_certificate=False, + allow_insecure_http=vtn.allow_insecure_http, + version=OADRVersion.OADR_310, + ) # type: ignore[return-value] + + +def seed_capacity_limit_event( + bl_client: BusinessLogicClient, + *, + target: str | None = None, +) -> ExistingEvent: + """ + Create a program and event with IMPORT/EXPORT_CAPACITY_LIMIT payloads. + + The event starts one hour from the current UTC time and spans 24 hours as + 96 consecutive 15-minute intervals. Each interval carries one import and one + export capacity limit value. FlexMeasures stores one belief per interval at the + interval start when the VEN fetch job runs. + """ + event_start = datetime.now(timezone.utc).replace(microsecond=0) + timedelta(hours=1) + event_duration = timedelta(hours=24) + payload_descriptors = ( + EventPayloadDescriptor( + payload_type=EventPayloadType.IMPORT_CAPACITY_LIMIT, + units=Unit.KW, + ), + EventPayloadDescriptor( + payload_type=EventPayloadType.EXPORT_CAPACITY_LIMIT, + units=Unit.KW, + ), + ) + + program = bl_client.programs.create_program( + NewProgram( + program_name=f"e2e-capacity-program-{uuid.uuid4().hex[:8]}", + payload_descriptors=payload_descriptors, + ) + ) + + intervals: Tuple[Interval[EventPayload], ...] = () + + for i in range(event_duration // timedelta(minutes=15)): + intervals += ( + Interval( + id=i, + interval_period=IntervalPeriod( + start=event_start + timedelta(minutes=i * 15), + duration=timedelta(minutes=15), + ), + payloads=( + EventPayload( + type=EventPayloadType.IMPORT_CAPACITY_LIMIT, + values=(random.uniform(0, 100),), + ), + EventPayload( + type=EventPayloadType.EXPORT_CAPACITY_LIMIT, + values=(random.uniform(0, 100),), + ), + ), + ), + ) + + return bl_client.events.create_event( + NewEvent( + programID=program.id, + event_name=f"e2e-capacity-event-{uuid.uuid4().hex[:8]}", + targets=() if target is None else (target,), + payload_descriptors=payload_descriptors, + intervals=intervals, + ) + ) diff --git a/tests/test_ven_client_repository.py b/tests/test_ven_client_repository.py new file mode 100644 index 0000000..073360e --- /dev/null +++ b/tests/test_ven_client_repository.py @@ -0,0 +1,181 @@ +"""Integration tests for VEN client and polling-schedule persistence.""" + +from __future__ import annotations + +import pytest +from flask_sqlalchemy import SQLAlchemy + +from flexmeasures_openadr3.models.storage import ( + VEN_CLIENT_ATTRIBUTE_KEY, + VenClientAttributePayload, +) +from flexmeasures_openadr3.utils.encryption import SecretsEncryptor +from flexmeasures_openadr3.utils.sensor import ( + EXPORT_CAPACITY_LIMIT_SENSOR_NAME, + IMPORT_CAPACITY_LIMIT_SENSOR_NAME, + VenSensorRepository, +) +from flexmeasures_openadr3.utils.ven_clients import ( + VenClient, + VenClientFormData, + VenClientRepository, + VenSensorConfigFormData, +) + + +def test_create_ven_client_persists_connection_attributes( + fresh_db: SQLAlchemy, + logged_in_prosumer: object, + ven_client_repository: VenClientRepository, + sample_ven_client_form_data: VenClientFormData, +) -> None: + """Creating a VEN client stores connection settings on the generic asset.""" + ven_client = ven_client_repository.create(sample_ven_client_form_data) + fresh_db.session.commit() + + reloaded = ven_client_repository.find_by_id(ven_client.id) + assert reloaded is not None + assert reloaded.name == sample_ven_client_form_data.name + assert reloaded.vtn_url == sample_ven_client_form_data.vtn_url + assert reloaded.oauth_token_url == sample_ven_client_form_data.oauth_token_url + assert reloaded.scopes == sample_ven_client_form_data.scopes + + payload = VenClientAttributePayload.from_asset_attributes(reloaded.asset.attributes) + assert payload.vtn_url == sample_ven_client_form_data.vtn_url + assert list(payload.scopes) == sample_ven_client_form_data.scopes + + +def test_create_ven_client_encrypts_oauth_credentials_in_attributes( + fresh_db: SQLAlchemy, + logged_in_prosumer: object, + ven_client_repository: VenClientRepository, + sample_ven_client_form_data: VenClientFormData, +) -> None: + """OAuth client id and secret are encrypted in the asset JSON attributes.""" + ven_client = ven_client_repository.create(sample_ven_client_form_data) + fresh_db.session.commit() + + raw = ven_client.asset.attributes[VEN_CLIENT_ATTRIBUTE_KEY] + assert raw["oauth_client_id"] != sample_ven_client_form_data.oauth_client_id + assert raw["oauth_client_secret"] != sample_ven_client_form_data.oauth_client_secret + + encryptor = SecretsEncryptor.from_current_app() + assert ( + encryptor.decrypt(raw["oauth_client_id"]) + == sample_ven_client_form_data.oauth_client_id + ) + assert ( + encryptor.decrypt(raw["oauth_client_secret"]) + == sample_ven_client_form_data.oauth_client_secret + ) + + +def test_append_sensor_config_persists_polling_schedule( + created_ven_client_with_schedule: VenClient, + ven_client_repository: VenClientRepository, + sample_sensor_config_form_data: VenSensorConfigFormData, +) -> None: + """A polling schedule is stored on the VEN asset and reloads correctly.""" + reloaded = ven_client_repository.find_by_id(created_ven_client_with_schedule.id) + assert reloaded is not None + + config = reloaded.get_sensor_config(sample_sensor_config_form_data.name) + assert config is not None + assert config.targets == sample_sensor_config_form_data.targets + assert config.utc_trigger_time == sample_sensor_config_form_data.utc_trigger_time + assert ( + config.fetch_import_capacity_limits + == sample_sensor_config_form_data.fetch_import_capacity_limits + ) + assert ( + config.fetch_export_capacity_limits + == sample_sensor_config_form_data.fetch_export_capacity_limits + ) + + payload = VenClientAttributePayload.from_asset_attributes(reloaded.asset.attributes) + assert len(payload.sensor_configs) == 1 + record = payload.sensor_configs[0] + assert record.name == sample_sensor_config_form_data.name + assert list(record.targets) == sample_sensor_config_form_data.targets + assert record.utc_trigger_time == sample_sensor_config_form_data.utc_trigger_time + + +def test_append_sensor_config_creates_import_and_export_sensors( + created_ven_client_with_schedule: VenClient, + sample_sensor_config_form_data: VenSensorConfigFormData, +) -> None: + """Import and export capacity-limit sensors are created on the VEN asset.""" + sensor_repo = VenSensorRepository() + asset = created_ven_client_with_schedule.asset + + import_sensor = sensor_repo.find_sensor( + sensor_repo.import_sensor_name(sample_sensor_config_form_data.name), + asset, + ) + export_sensor = sensor_repo.find_sensor( + sensor_repo.export_sensor_name(sample_sensor_config_form_data.name), + asset, + ) + + assert import_sensor is not None + assert export_sensor is not None + assert import_sensor.name.startswith(IMPORT_CAPACITY_LIMIT_SENSOR_NAME) + assert export_sensor.name.startswith(EXPORT_CAPACITY_LIMIT_SENSOR_NAME) + assert import_sensor.generic_asset_id == asset.id + assert export_sensor.generic_asset_id == asset.id + + +def test_update_ven_client_persists_changed_fields( + fresh_db: SQLAlchemy, + created_ven_client: VenClient, + ven_client_repository: VenClientRepository, +) -> None: + """Updating a VEN client overwrites stored connection attributes.""" + updated_form = VenClientFormData( + name="renamed-ven-client", + vtn_url="https://vtn.example/v2", + oauth_client_id="new-client-id", + oauth_client_secret="new-client-secret", + oauth_token_url="https://vtn.example/oauth/v2", + scopes=["openid"], + ) + + ven_client_repository.update(created_ven_client, updated_form) + fresh_db.session.commit() + + reloaded = ven_client_repository.find_by_name("renamed-ven-client") + assert reloaded is not None + assert reloaded.vtn_url == updated_form.vtn_url + assert reloaded.scopes == updated_form.scopes + + +def test_delete_sensor_config_removes_schedule_from_attributes( + fresh_db: SQLAlchemy, + created_ven_client_with_schedule: VenClient, + ven_client_repository: VenClientRepository, + sample_sensor_config_form_data: VenSensorConfigFormData, +) -> None: + """Deleting a polling schedule removes it from the VEN asset attributes.""" + ven_client_repository.delete_sensor_config( + created_ven_client_with_schedule, + sample_sensor_config_form_data.name, + ) + fresh_db.session.commit() + + reloaded = ven_client_repository.find_by_id(created_ven_client_with_schedule.id) + assert reloaded is not None + assert reloaded.get_sensor_config(sample_sensor_config_form_data.name) is None + + payload = VenClientAttributePayload.from_asset_attributes(reloaded.asset.attributes) + assert payload.sensor_configs == () + + +def test_duplicate_ven_client_name_raises( + created_ven_client: VenClient, + logged_in_prosumer: object, + ven_client_repository: VenClientRepository, + sample_ven_client_form_data: VenClientFormData, +) -> None: + """Two VEN clients cannot share the same asset name.""" + with pytest.raises(ValueError, match="already exists"): + ven_client_repository.create(sample_ven_client_form_data) diff --git a/tests/test_ven_fetch_e2e.py b/tests/test_ven_fetch_e2e.py new file mode 100644 index 0000000..c997b60 --- /dev/null +++ b/tests/test_ven_fetch_e2e.py @@ -0,0 +1,172 @@ +"""End-to-end test: VTN capacity events -> FlexMeasures VEN fetch job -> sensor beliefs.""" + +from __future__ import annotations + + +from openadr3_client.oadr310.models.event.event import ExistingEvent +from openadr3_client.oadr310.models.event.event_payload import EventPayloadType +import pytest +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import select + +from flexmeasures.data.models.time_series import TimedBelief +from flexmeasures.data.services.data_sources import get_or_create_source + +from flexmeasures_openadr3.utils.ven_clients import ( + VenClientFormData, + VenClientRepository, + VenSensorConfigFormData, +) +from flexmeasures_openadr3.utils.ven_jobs import ( + OPENADR_EVENT_SOURCE_NAME, + OPENADR_EVENT_SOURCE_TYPE, + VenFetchJobScheduler, +) + +from tests.test_container.integration_types import IntegrationTestVTNClient + + +def _build_ven_client_form_data( + ven_client_fixture: IntegrationTestVTNClient, +) -> VenClientFormData: + oauth = ven_client_fixture.oauth_configuration + return VenClientFormData( + name="e2e-ven-client", + vtn_url=ven_client_fixture.vtn_configuration.vtn_base_url, + oauth_client_id=oauth.client_id, + oauth_client_secret=oauth.client_secret, + oauth_token_url=oauth.token_url, + scopes=[], # use Keycloak default client scopes (avoids oauthlib scope-mismatch warnings) + ) + + +def _run_fetch_job_immediately( + app: Flask, + ven_client_repository: VenClientRepository, + ven_id: int, + config_name: str, +) -> None: + """Invoke the fetch-events worker logic synchronously (skip RQ schedule delay).""" + scheduler = VenFetchJobScheduler(ven_client_repository) + with app.app_context(): + scheduler._execute(ven_id, config_name) + + +def _beliefs_for_sensor(db: SQLAlchemy, sensor_id: int) -> list[TimedBelief]: + source = get_or_create_source( + source=OPENADR_EVENT_SOURCE_NAME, + source_type=OPENADR_EVENT_SOURCE_TYPE, + ) + return list( + db.session.scalars( + select(TimedBelief) + .filter_by(sensor_id=sensor_id, source_id=source.id) + .order_by(TimedBelief.event_start) + ).all() + ) + + +def test_ven_fetch_events_e2e_stores_openadr_capacity_limits( + app: Flask, + fresh_db: SQLAlchemy, + logged_in_prosumer: object, + ven_client: IntegrationTestVTNClient, + vtn_capacity_limit_event_seed: ExistingEvent, + ven_client_repository: VenClientRepository, +) -> None: + """ + Full flow: create VEN + polling schedule, run fetch job, verify sensor beliefs. + + OpenADR setup (session fixture): BL client seeds a 24-hour program/event on the VTN + with 96 fifteen-minute intervals, each carrying IMPORT/EXPORT_CAPACITY_LIMIT + payloads. The event starts one hour after the session fixture is created. + + The fetch job stores one TimedBelief per interval at the interval start on dedicated + import/export sensors (15-minute sensor resolution). Assertions verify belief count, + that each belief falls within its seed interval, payload values match, and that + event_start times run from the first interval start through the last interval start. + """ + seed = vtn_capacity_limit_event_seed + + # --- FlexMeasures: VEN client and polling schedule --- + ven = ven_client_repository.create(_build_ven_client_form_data(ven_client)) + schedule_form = VenSensorConfigFormData( + name="e2e-poll", + targets=[], + utc_trigger_time="00:00:00", + fetch_import_capacity_limits=True, + fetch_export_capacity_limits=True, + ) + ven_client_repository.append_sensor_config(ven, schedule_form) + fresh_db.session.commit() + + updated_ven = ven_client_repository.find_by_id(ven.id) + assert updated_ven is not None + config = updated_ven.get_sensor_config("e2e-poll") + assert config is not None + assert config.import_sensor is not None + assert config.export_sensor is not None + + # --- Run fetch job immediately (do not wait for the daily RQ trigger) --- + _run_fetch_job_immediately(app, ven_client_repository, ven.id, config.name) + + # --- Assert beliefs on import/export sensors --- + import_beliefs = _beliefs_for_sensor(fresh_db, config.import_sensor.id) + export_beliefs = _beliefs_for_sensor(fresh_db, config.export_sensor.id) + + expected_count = len(seed.intervals or ()) + assert len(import_beliefs) == expected_count + assert len(export_beliefs) == expected_count + + for i, belief in enumerate(import_beliefs): + interval = seed.intervals[i] if seed.intervals is not None else None + assert interval is not None and interval.interval_period is not None + assert ( + interval.interval_period.start + <= belief.event_start + < interval.interval_period.start + interval.interval_period.duration + ) + + import_capacity_payload = next( + ( + payload + for payload in interval.payloads + if payload.type == EventPayloadType.IMPORT_CAPACITY_LIMIT + ), + None, + ) + assert import_capacity_payload is not None + assert belief.event_value == pytest.approx(import_capacity_payload.values[0]) + + for i, belief in enumerate(export_beliefs): + interval = seed.intervals[i] if seed.intervals is not None else None + assert interval is not None and interval.interval_period is not None + assert ( + interval.interval_period.start + <= belief.event_start + < interval.interval_period.start + interval.interval_period.duration + ) + + export_capacity_payload = next( + ( + payload + for payload in interval.payloads + if payload.type == EventPayloadType.EXPORT_CAPACITY_LIMIT + ), + None, + ) + assert export_capacity_payload is not None + assert belief.event_value == pytest.approx(export_capacity_payload.values[0]) + + # Interval spans ~24 hours into the future from fixture creation. + min_start = min(b.event_start for b in import_beliefs) + max_start = max(b.event_start for b in import_beliefs) + + initial_interval = seed.intervals[0] if seed.intervals is not None else None + assert initial_interval is not None and initial_interval.interval_period is not None + last_interval = seed.intervals[-1] if seed.intervals is not None else None + assert last_interval is not None and last_interval.interval_period is not None + + assert min_start == initial_interval.interval_period.start + assert max_start == last_interval.interval_period.start diff --git a/tests/test_ven_fetch_jobs.py b/tests/test_ven_fetch_jobs.py new file mode 100644 index 0000000..95b3166 --- /dev/null +++ b/tests/test_ven_fetch_jobs.py @@ -0,0 +1,160 @@ +"""Integration tests for RQ scheduling of daily fetch-events jobs.""" + +from __future__ import annotations + +import pytest +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from rq.job import Job + +from flexmeasures_openadr3.utils.ven_clients import ( + VenClient, + VenClientRepository, + VenSensorConfig, +) +from flexmeasures_openadr3.utils.ven_jobs import ( + FETCH_EVENTS_QUEUE_NAME, + VenFetchJobScheduler, +) + + +def test_schedule_enqueues_job_in_forecasting_queue( + app: Flask, + fresh_db: SQLAlchemy, + clean_forecasting_queue: None, + created_ven_client_with_schedule: VenClient, + ven_client_repository: VenClientRepository, + ven_fetch_job_scheduler: VenFetchJobScheduler, +) -> None: + """Scheduling a polling schedule enqueues an RQ job on the forecasting queue.""" + reloaded = ven_client_repository.find_by_id(created_ven_client_with_schedule.id) + assert reloaded is not None + + config = reloaded.get_sensor_config("daily-poll") + assert config is not None + + job = ven_fetch_job_scheduler.schedule(reloaded, config) + fresh_db.session.commit() + + assert job is not None + assert job.id is not None + assert job.origin == FETCH_EVENTS_QUEUE_NAME + + queued_job = Job.fetch( + job.id, connection=app.queues[FETCH_EVENTS_QUEUE_NAME].connection + ) + assert queued_job.id == job.id + assert queued_job.kwargs == { + "ven_id": reloaded.id, + "config_name": config.name, + } + + +def test_schedule_persists_job_id_on_sensor_config( + fresh_db: SQLAlchemy, + clean_forecasting_queue: None, + created_ven_client_with_schedule: VenClient, + ven_client_repository: VenClientRepository, + ven_fetch_job_scheduler: VenFetchJobScheduler, +) -> None: + """After scheduling, the job id is stored on the polling schedule record.""" + reloaded = ven_client_repository.find_by_id(created_ven_client_with_schedule.id) + assert reloaded is not None + + config = reloaded.get_sensor_config("daily-poll") + assert config is not None + + job = ven_fetch_job_scheduler.schedule(reloaded, config) + fresh_db.session.commit() + + assert job is not None + + persisted = ven_client_repository.find_by_id(reloaded.id) + assert persisted is not None + persisted_config = persisted.get_sensor_config("daily-poll") + assert persisted_config is not None + assert persisted_config.fetch_events_job_id == job.id + + +def test_schedule_without_utc_trigger_time_returns_none( + fresh_db: SQLAlchemy, + created_ven_client: VenClient, + ven_client_repository: VenClientRepository, + ven_fetch_job_scheduler: VenFetchJobScheduler, +) -> None: + """An empty UTC trigger time does not enqueue a job.""" + config = VenSensorConfig( + name="no-trigger", + targets=["t1"], + utc_trigger_time="", + fetch_import_capacity_limits=True, + ) + created_ven_client.sensor_configs.append(config) + ven_client_repository._persist_payload(created_ven_client) + fresh_db.session.commit() + + job = ven_fetch_job_scheduler.schedule(created_ven_client, config) + assert job is None + + +def test_delete_removes_scheduled_job_from_queue( + app: Flask, + fresh_db: SQLAlchemy, + clean_forecasting_queue: None, + created_ven_client_with_schedule: VenClient, + ven_client_repository: VenClientRepository, + ven_fetch_job_scheduler: VenFetchJobScheduler, +) -> None: + """Deleting a schedule removes its RQ job from Redis.""" + reloaded = ven_client_repository.find_by_id(created_ven_client_with_schedule.id) + assert reloaded is not None + + config = reloaded.get_sensor_config("daily-poll") + assert config is not None + + job = ven_fetch_job_scheduler.schedule(reloaded, config) + fresh_db.session.commit() + assert job is not None + + persisted = ven_client_repository.find_by_id(reloaded.id) + assert persisted is not None + config_with_job = persisted.get_sensor_config("daily-poll") + assert config_with_job is not None + + ven_fetch_job_scheduler.delete(config_with_job) + + from rq.exceptions import NoSuchJobError + + with pytest.raises(NoSuchJobError): + Job.fetch(job.id, connection=app.queues[FETCH_EVENTS_QUEUE_NAME].connection) + + +def test_delete_all_removes_jobs_for_every_schedule( + app: Flask, + fresh_db: SQLAlchemy, + clean_forecasting_queue: None, + created_ven_client_with_schedule: VenClient, + ven_client_repository: VenClientRepository, + ven_fetch_job_scheduler: VenFetchJobScheduler, +) -> None: + """delete_all clears scheduled jobs for all polling schedules on a VEN client.""" + reloaded = ven_client_repository.find_by_id(created_ven_client_with_schedule.id) + assert reloaded is not None + + for config in reloaded.sensor_configs: + ven_fetch_job_scheduler.schedule(reloaded, config) + fresh_db.session.commit() + + persisted = ven_client_repository.find_by_id(reloaded.id) + assert persisted is not None + job_ids = [cfg.fetch_events_job_id for cfg in persisted.sensor_configs] + assert all(job_id is not None for job_id in job_ids) + + ven_fetch_job_scheduler.delete_all(persisted) + + from rq.exceptions import NoSuchJobError + + for job_id in job_ids: + assert job_id is not None + with pytest.raises(NoSuchJobError): + Job.fetch(job_id, connection=app.queues[FETCH_EVENTS_QUEUE_NAME].connection) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..abd71eb --- /dev/null +++ b/uv.lock @@ -0,0 +1,3807 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "(python_full_version >= '3.15' and os_name == 'nt') or (python_full_version >= '3.15' and os_name == 'win')", + "(python_full_version == '3.14.*' and os_name == 'nt') or (python_full_version == '3.14.*' and os_name == 'win')", + "(python_full_version < '3.14' and os_name == 'nt') or (python_full_version < '3.14' and os_name == 'win')", + "python_full_version >= '3.15' and os_name != 'nt' and os_name != 'win'", + "python_full_version == '3.14.*' and os_name != 'nt' and os_name != 'win'", + "python_full_version < '3.14' and os_name != 'nt' and os_name != 'win'", +] + +[[package]] +name = "aiofiles" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, +] + +[[package]] +name = "alembic" +version = "1.18.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, +] + +[[package]] +name = "altair" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "narwhals" }, + { name = "packaging" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/1e/365a9144db3254f86f1b974660b9ede1e9a38c9dc0730e4a9b1192eec5d6/altair-6.1.0.tar.gz", hash = "sha256:dda699216cf85b040d968ae5a569ad45957616811e38760a85e5118269daca67", size = 765519, upload-time = "2026-04-21T13:08:46.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/63/5dacc8d8306c715088b897a479e551bc0779fd2f0f26c97fec5e36542b4e/altair-6.1.0-py3-none-any.whl", hash = "sha256:fdf5fd939512e5b2fc4441c82dfd2635e706defbd037db0ac429ef5ddce66c3b", size = 796996, upload-time = "2026-04-21T13:08:48.549Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "apispec" +version = "6.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/f1/1f5a9332df3ecd90cc5ab69bc58a4174b8ba2ac1720c4c26b01d20751bf5/apispec-6.10.0.tar.gz", hash = "sha256:0a888555cd4aa5fb7176041be15684154fd8961055e1672e703abf737e8761bf", size = 80631, upload-time = "2026-03-06T21:48:40.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/88/e149b20246c4689e7d27163e4e3bb8946ef31617cfb3b9c427813483fe5b/apispec-6.10.0-py3-none-any.whl", hash = "sha256:8ff23e0de9a0ceb62ff70047241126315bd17b8d0565a567934c0156f4ddbb43", size = 31313, upload-time = "2026-03-06T21:48:39.404Z" }, +] + +[package.optional-dependencies] +yaml = [ + { name = "pyyaml" }, +] + +[[package]] +name = "apispec-oneofschema" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apispec" }, + { name = "marshmallow" }, + { name = "marshmallow-oneofschema" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/79/9bc6993ade0a3b841812cd46523ecf244bbb9deacb14d29d04eff541a2c7/apispec_oneofschema-3.0.2.tar.gz", hash = "sha256:03fad9b6d88f0e61669700d2e544e1410064d3cc267dc5d1d174b26f69bd05b7", size = 3677, upload-time = "2025-07-28T06:11:24.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/9b/bbad099ba044be05c8ccfa3414f9b80bbc3c989873da433036f8704f51c2/apispec_oneofschema-3.0.2-py3-none-any.whl", hash = "sha256:7acc177b0fb482d66c4d9e2acc16952fa4c219be29e6e60783bcc7793fb068eb", size = 3079, upload-time = "2025-07-28T06:11:22.849Z" }, +] + +[[package]] +name = "apispec-webframeworks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apispec", extra = ["yaml"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/1f/8d98328269a2507b6cfe37e8a70725d168626615865dc7aec30e8720c495/apispec_webframeworks-1.2.0.tar.gz", hash = "sha256:5689288c266a2713c2f516eacc14ea2fec9b21f193edc8f659c770342b97fd81", size = 10837, upload-time = "2024-09-16T19:01:18.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/2c/5aa396a3bca60de3b3f419ea770b8087d52aac5221bf15ddcd9b48d61d3c/apispec_webframeworks-1.2.0-py3-none-any.whl", hash = "sha256:68aea0d1eeb3caeeacc7d6772a48c59c8b60b1a88d0bd51529d94597ccf33116", size = 9119, upload-time = "2024-09-16T19:01:16.402Z" }, +] + +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, + { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, +] + +[[package]] +name = "arrow" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +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 = "bcrypt" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697, upload-time = "2025-02-28T01:24:09.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/2c/3d44e853d1fe969d229bd58d39ae6902b3d924af0e2b5a60d17d4b809ded/bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", size = 483719, upload-time = "2025-02-28T01:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e2/58ff6e2a22eca2e2cff5370ae56dba29d70b1ea6fc08ee9115c3ae367795/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", size = 272001, upload-time = "2025-02-28T01:22:38.078Z" }, + { url = "https://files.pythonhosted.org/packages/37/1f/c55ed8dbe994b1d088309e366749633c9eb90d139af3c0a50c102ba68a1a/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", size = 277451, upload-time = "2025-02-28T01:22:40.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1c/794feb2ecf22fe73dcfb697ea7057f632061faceb7dcf0f155f3443b4d79/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", size = 272792, upload-time = "2025-02-28T01:22:43.144Z" }, + { url = "https://files.pythonhosted.org/packages/13/b7/0b289506a3f3598c2ae2bdfa0ea66969812ed200264e3f61df77753eee6d/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", size = 289752, upload-time = "2025-02-28T01:22:45.56Z" }, + { url = "https://files.pythonhosted.org/packages/dc/24/d0fb023788afe9e83cc118895a9f6c57e1044e7e1672f045e46733421fe6/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", size = 277762, upload-time = "2025-02-28T01:22:47.023Z" }, + { url = "https://files.pythonhosted.org/packages/e4/38/cde58089492e55ac4ef6c49fea7027600c84fd23f7520c62118c03b4625e/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", size = 272384, upload-time = "2025-02-28T01:22:49.221Z" }, + { url = "https://files.pythonhosted.org/packages/de/6a/d5026520843490cfc8135d03012a413e4532a400e471e6188b01b2de853f/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", size = 277329, upload-time = "2025-02-28T01:22:51.603Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a3/4fc5255e60486466c389e28c12579d2829b28a527360e9430b4041df4cf9/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", size = 305241, upload-time = "2025-02-28T01:22:53.283Z" }, + { url = "https://files.pythonhosted.org/packages/c7/15/2b37bc07d6ce27cc94e5b10fd5058900eb8fb11642300e932c8c82e25c4a/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", size = 309617, upload-time = "2025-02-28T01:22:55.461Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1f/99f65edb09e6c935232ba0430c8c13bb98cb3194b6d636e61d93fe60ac59/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", size = 335751, upload-time = "2025-02-28T01:22:57.81Z" }, + { url = "https://files.pythonhosted.org/packages/00/1b/b324030c706711c99769988fcb694b3cb23f247ad39a7823a78e361bdbb8/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", size = 355965, upload-time = "2025-02-28T01:22:59.181Z" }, + { url = "https://files.pythonhosted.org/packages/aa/dd/20372a0579dd915dfc3b1cd4943b3bca431866fcb1dfdfd7518c3caddea6/bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", size = 155316, upload-time = "2025-02-28T01:23:00.763Z" }, + { url = "https://files.pythonhosted.org/packages/6d/52/45d969fcff6b5577c2bf17098dc36269b4c02197d551371c023130c0f890/bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", size = 147752, upload-time = "2025-02-28T01:23:02.908Z" }, + { url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019, upload-time = "2025-02-28T01:23:05.838Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174, upload-time = "2025-02-28T01:23:07.274Z" }, + { url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870, upload-time = "2025-02-28T01:23:09.151Z" }, + { url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601, upload-time = "2025-02-28T01:23:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660, upload-time = "2025-02-28T01:23:12.989Z" }, + { url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083, upload-time = "2025-02-28T01:23:14.5Z" }, + { url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237, upload-time = "2025-02-28T01:23:16.686Z" }, + { url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737, upload-time = "2025-02-28T01:23:18.897Z" }, + { url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741, upload-time = "2025-02-28T01:23:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472, upload-time = "2025-02-28T01:23:23.183Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606, upload-time = "2025-02-28T01:23:25.361Z" }, + { url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867, upload-time = "2025-02-28T01:23:26.875Z" }, + { url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589, upload-time = "2025-02-28T01:23:28.381Z" }, + { url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794, upload-time = "2025-02-28T01:23:30.187Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969, upload-time = "2025-02-28T01:23:31.945Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158, upload-time = "2025-02-28T01:23:34.161Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285, upload-time = "2025-02-28T01:23:35.765Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583, upload-time = "2025-02-28T01:23:38.021Z" }, + { url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896, upload-time = "2025-02-28T01:23:39.575Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492, upload-time = "2025-02-28T01:23:40.901Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213, upload-time = "2025-02-28T01:23:42.653Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162, upload-time = "2025-02-28T01:23:43.964Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856, upload-time = "2025-02-28T01:23:46.011Z" }, + { url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726, upload-time = "2025-02-28T01:23:47.575Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664, upload-time = "2025-02-28T01:23:49.059Z" }, + { url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128, upload-time = "2025-02-28T01:23:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598, upload-time = "2025-02-28T01:23:51.775Z" }, + { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload-time = "2025-02-28T01:23:53.139Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "cbor2" +version = "5.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/cb/09939728be094d155b5d4ac262e39877875f5f7e36eea66beb359f647bd0/cbor2-5.9.0.tar.gz", hash = "sha256:85c7a46279ac8f226e1059275221e6b3d0e370d2bb6bd0500f9780781615bcea", size = 111231, upload-time = "2026-03-22T15:56:50.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/39/72d8a5a4b06565561ec28f4fcb41aff7bb77f51705c01f00b8254a2aca4f/cbor2-5.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f223dffb1bcdd2764665f04c1152943d9daa4bc124a576cd8dee1cad4264313", size = 71223, upload-time = "2026-03-22T15:56:13.68Z" }, + { url = "https://files.pythonhosted.org/packages/09/fd/7ddf3d3153b54c69c3be77172b8d9aa3a9d74f62a7fbde614d53eaeed9a4/cbor2-5.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae6c706ac1d85a0b3cb3395308fd0c4d55e3202b4760773675957e93cdff45fc", size = 287865, upload-time = "2026-03-22T15:56:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/db/9d/7ede2cc42f9bb4260492e7d29d2aab781eacbbcfb09d983de1e695077199/cbor2-5.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4cd43d8fc374b31643b2830910f28177a606a7bc84975a62675dd3f2e320fc7b", size = 288246, upload-time = "2026-03-22T15:56:16.113Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9d/588ebc7c5bc5843f609b05fe07be8575c7dec987735b0bbc908ac9c1264a/cbor2-5.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4aa07b392cc3d76fb31c08a46a226b58c320d1c172ff3073e864409ced7bc50f", size = 280214, upload-time = "2026-03-22T15:56:17.519Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a1/6fc8f4b15c6a27e7fbb7966c30c2b4b18c274a3221fa2f5e6235502d34bc/cbor2-5.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:971d425b3a23b75953d8853d5f9911bdeefa09d759ee3b5e6b07b5ff3cbd9073", size = 282162, upload-time = "2026-03-22T15:56:18.975Z" }, + { url = "https://files.pythonhosted.org/packages/cf/20/9a22cfe08be16ddfeef2542cf4eeed1b29f3f57ddbba0b42f7e0bb8331fd/cbor2-5.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:34a6cb15e6ab6a8eae94ad2041731cd3ef786af43a8df99f847969af5b902ee7", size = 70049, upload-time = "2026-03-22T15:56:20.502Z" }, + { url = "https://files.pythonhosted.org/packages/c6/9e/695f92d09006614034e25a9f5b10620f3b219f79c1bec3c37b7c6f27a7a9/cbor2-5.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:7d1ddc4541e7367ac58c2470cc0df847f7137167fe4f5729e2d3cc0b993d7da4", size = 65382, upload-time = "2026-03-22T15:56:21.526Z" }, + { url = "https://files.pythonhosted.org/packages/81/c5/4901e21a8afe9448fd947b11e8f383903207cd6dd0800e5f5a386838de5b/cbor2-5.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fbb06f34aa645b4deca66643bba3d400d20c15312d1fe88d429be60c1ab50f27", size = 71284, upload-time = "2026-03-22T15:56:22.836Z" }, + { url = "https://files.pythonhosted.org/packages/1b/10/df643a381aebc3f05486de4813662bc58accb640fc3275cb276a75e89694/cbor2-5.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac684fe195c39821fca70d18afbf748f728aefbfbf88456018d299e559b8cae0", size = 287682, upload-time = "2026-03-22T15:56:24.024Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/8aa6b766059ae4a0ca1ec3ff96fe3823a69a7be880dba2e249f7fbe2700b/cbor2-5.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a54fbb32cb828c214f7f333a707e4aec61182e7efdc06ea5d9596d3ecee624a", size = 288009, upload-time = "2026-03-22T15:56:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/74/07/6236bc25c183a9cf7e8062e5dddf9eae9b0b14ebf14a58a69fe5a1e872c6/cbor2-5.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4753a6d1bc71054d9179557bc65740860f185095ccb401d46637fff028a5b3ec", size = 280437, upload-time = "2026-03-22T15:56:26.479Z" }, + { url = "https://files.pythonhosted.org/packages/4e/0a/84328d23c3c68874ac6497edb9b1900579a1028efa54734df3f1762bbc15/cbor2-5.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:380e534482b843e43442b87d8777a7bf9bed20cb7526f89b780c3400f617304b", size = 282247, upload-time = "2026-03-22T15:56:28.644Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f6/89b4627e09d028c8e5fcaf7cb55f225c33ce6e037ec1844e65d02bcfa945/cbor2-5.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:dcf0f695873e5c94bd072d6af8698e72b8fb7f7a18f37e0bced1041b7111a6cf", size = 70089, upload-time = "2026-03-22T15:56:29.801Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7c/efadcd5f0102db692490e4e206988a2f98d39a09912090db497a2b800885/cbor2-5.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:f7c9751a9611601ab326d8f5837f01379195bbf06175fb4effeb552140e7c9e8", size = 65466, upload-time = "2026-03-22T15:56:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/08/7d/9ccc36d10ef96e6038e48046ebe1ce35a1e7814da0e1e204d09e6ef09b8d/cbor2-5.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23606d31ba1368bd1b6602e3020ee88fe9523ca80e8630faf6b2fc904fd84560", size = 71500, upload-time = "2026-03-22T15:56:31.876Z" }, + { url = "https://files.pythonhosted.org/packages/70/e1/a6cca2cc72e13f00030c6a649f57ae703eb2c620806ab70c40db8eab33fa/cbor2-5.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0322296b9d52f55880e300ba8ba09ecf644303b99b51138bbb1c0fb644fa7c3e", size = 286953, upload-time = "2026-03-22T15:56:33.292Z" }, + { url = "https://files.pythonhosted.org/packages/08/3c/24cd5ef488a957d90e016f200a3aad820e4c2f85edd61c9fe4523007a1ee/cbor2-5.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:422817286c1d0ce947fb2f7eca9212b39bddd7231e8b452e2d2cc52f15332dba", size = 285454, upload-time = "2026-03-22T15:56:34.703Z" }, + { url = "https://files.pythonhosted.org/packages/a4/35/dca96818494c0ba47cdd73e8d809b27fa91f8fa0ce32a068a09237687454/cbor2-5.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9a4907e0c3035bb8836116854ed8e56d8aef23909d601fa59706320897ec2551", size = 279441, upload-time = "2026-03-22T15:56:35.888Z" }, + { url = "https://files.pythonhosted.org/packages/a4/44/d3362378b16e53cf7e535a3f5aed8476e2109068154e24e31981ef5bde9e/cbor2-5.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fb7afe77f8d269e42d7c4b515c6fd14f1ccc0625379fb6829b269f493d16eddd", size = 279673, upload-time = "2026-03-22T15:56:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/43/d1/3533a697e5842fff7c2f64912eb251f8dcab3a8b5d88e228d6eebc3b5021/cbor2-5.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:86baf870d4c0bfc6f79de3801f3860a84ab76d9c8b0abb7f081f2c14c38d79d3", size = 71940, upload-time = "2026-03-22T15:56:38.366Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e2/c6ba75f3fb25dfa15ab6999cc8709c821987e9ed8e375d7f58539261bcb9/cbor2-5.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:7221483fad0c63afa4244624d552abf89d7dfdbc5f5edfc56fc1ff2b4b818975", size = 67639, upload-time = "2026-03-22T15:56:39.39Z" }, + { url = "https://files.pythonhosted.org/packages/42/ff/b83492b096fbef26e9cb62c1a4bf2d3cef579ea7b33138c6c37c4ae66f67/cbor2-5.9.0-py3-none-any.whl", hash = "sha256:27695cbd70c90b8de5c4a284642c2836449b14e2c2e07e3ffe0744cb7669a01b", size = 24627, upload-time = "2026-03-22T15:56:48.847Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click-default-group" +version = "1.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/ce/edb087fb53de63dad3b36408ca30368f438738098e668b78c87f93cd41df/click_default_group-1.2.4.tar.gz", hash = "sha256:eb3f3c99ec0d456ca6cd2a7f08f7d4e91771bef51b01bdd9580cc6450fe1251e", size = 3505, upload-time = "2023-08-04T07:54:58.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/1a/aff8bb287a4b1400f69e09a53bd65de96aa5cee5691925b38731c67fc695/click_default_group-1.2.4-py2.py3-none-any.whl", hash = "sha256:9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f", size = 4123, upload-time = "2023-08-04T07:54:56.875Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, +] + +[[package]] +name = "convertdate" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pymeeus" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/8d/540bf9cdbcd59352ecf7a1925197881465a6c24fd6171765dc11bbeed19e/convertdate-2.4.1.tar.gz", hash = "sha256:ace904a9d0230742732471748160b99d6ae881c2d0dc9a2463c7a37340c56c91", size = 52505, upload-time = "2026-02-08T00:37:53.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/53/63d707d9de41aab3c51faa4f0e9234c36ee8c63f56c90c3ae23a01976e81/convertdate-2.4.1-py3-none-any.whl", hash = "sha256:0622acf8bc600956b937e8dd7cee36ba035669b62d2db224bf130af1fc84f754", size = 48440, upload-time = "2026-02-08T00:37:52.405Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + +[[package]] +name = "croniter" +version = "6.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/de/5832661ed55107b8a09af3f0a2e71e0957226a59eb1dcf0a445cce6daf20/croniter-6.2.2.tar.gz", hash = "sha256:ba60832a5ec8e12e51b8691c3309a113d1cf6526bdf1a48150ce8ec7a532d0ab", size = 113762, upload-time = "2026-03-15T08:43:48.112Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/39/783980e78cb92c2d7bdb1fc7dbc86e94ccc6d58224d76a7f1f51b6c51e30/croniter-6.2.2-py3-none-any.whl", hash = "sha256:a5d17b1060974d36251ea4faf388233eca8acf0d09cbd92d35f4c4ac8f279960", size = 45422, upload-time = "2026-03-15T08:43:46.626Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, + { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, + { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, + { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "deprecation" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788, upload-time = "2020-04-20T14:23:38.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, +] + +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "docker" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "fakeredis" +version = "2.35.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "redis" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/50/b748233c02fa77e5105238190cc9bb58b852eb1c8b1d0763230d3a5b745a/fakeredis-2.35.1.tar.gz", hash = "sha256:5bae5eba7b9d93cb968944ac40936373cf2397ff71667d4b595df65c3d2e413f", size = 189118, upload-time = "2026-04-12T17:05:58.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/27/b8b057a23f7777177e92d3a602fd866751b6b45014964548997e92e048fd/fakeredis-2.35.1-py3-none-any.whl", hash = "sha256:67d97e11f562b7870e11e5c30cf182270bfb2dd37f6707dba47cc6d91628d1b9", size = 129678, upload-time = "2026-04-12T17:05:56.86Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, +] + +[[package]] +name = "flask" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, +] + +[[package]] +name = "flask-classful" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/9f/0a2e2144e3e32d1d8e5bd66acb3cb54f76b7072ab832a6be9b7d707d2015/flask_classful-0.16.0.tar.gz", hash = "sha256:9073dc705f59e301da84d981f48bdff1901a5170700b685f42548d6f754156d5", size = 8806, upload-time = "2023-09-07T21:44:49.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/c4/2d11d7b9190e478107078f158ba69cb0fe5c5632ad486c1115cc6d3a4163/flask_classful-0.16.0-py3-none-any.whl", hash = "sha256:4a2f9020588caf62ee0662e98b02503a99b2422ed29e9832c85f143f652e6ed1", size = 9482, upload-time = "2023-09-07T21:44:48.135Z" }, +] + +[[package]] +name = "flask-cors" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/74/0fc0fa68d62f21daef41017dafab19ef4b36551521260987eb3a5394c7ba/flask_cors-6.0.2.tar.gz", hash = "sha256:6e118f3698249ae33e429760db98ce032a8bf9913638d085ca0f4c5534ad2423", size = 13472, upload-time = "2025-12-12T20:31:42.861Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/af/72ad54402e599152de6d067324c46fe6a4f531c7c65baf7e96c63db55eaf/flask_cors-6.0.2-py3-none-any.whl", hash = "sha256:e57544d415dfd7da89a9564e1e3a9e515042df76e12130641ca6f3f2f03b699a", size = 13257, upload-time = "2025-12-12T20:31:41.3Z" }, +] + +[[package]] +name = "flask-json" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/4a/6046e195241772f4b6add3adcd4e820004c6151afc587d8c7d0d4ffeb473/Flask-JSON-0.4.0.tar.gz", hash = "sha256:07945d66024f3b77694ce1db5d1fe83940f2aa3bcad8a608535686be67e4bc48", size = 15899, upload-time = "2023-05-06T07:45:52.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/87/3b3ae3ca23dd90b15faa1128c473c1cf16d42e9f95efc5e4a1b77bf22260/Flask_JSON-0.4.0-py3-none-any.whl", hash = "sha256:1c1b87a657daa2179fc19f1ffc78204a716c7c5139673dc5038772db4d9f1988", size = 8714, upload-time = "2023-05-06T07:45:50.883Z" }, +] + +[[package]] +name = "flask-login" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/6e/2f4e13e373bb49e68c02c51ceadd22d172715a06716f9299d9df01b6ddb2/Flask-Login-0.6.3.tar.gz", hash = "sha256:5e23d14a607ef12806c699590b89d0f0e0d67baeec599d75947bf9c147330333", size = 48834, upload-time = "2023-10-30T14:53:21.151Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/f5/67e9cc5c2036f58115f9fe0f00d203cf6780c3ff8ae0e705e7a9d9e8ff9e/Flask_Login-0.6.3-py3-none-any.whl", hash = "sha256:849b25b82a436bf830a054e74214074af59097171562ab10bfa999e6b78aae5d", size = 17303, upload-time = "2023-10-30T14:53:19.636Z" }, +] + +[[package]] +name = "flask-mail" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "flask" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/29/e92dc84c675d1e8d260d5768eb3fb65c70cbd33addecf424187587bee862/flask_mail-0.10.0.tar.gz", hash = "sha256:44083e7b02bbcce792209c06252f8569dd5a325a7aaa76afe7330422bd97881d", size = 8152, upload-time = "2024-05-23T22:30:12.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/c0/a81083da779f482494d49195d8b6c9fde21072558253e4a9fb2ec969c3c1/flask_mail-0.10.0-py3-none-any.whl", hash = "sha256:a451e490931bb3441d9b11ebab6812a16bfa81855792ae1bf9c1e1e22c4e51e7", size = 8529, upload-time = "2024-05-23T22:30:10.962Z" }, +] + +[[package]] +name = "flask-marshmallow" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "marshmallow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/ca/22f00dffdd6709332247960e0c1223995bc22ff7537834bffc44ab9ab90b/flask_marshmallow-1.5.0.tar.gz", hash = "sha256:7c06b56e41647eccdb3cd57c25b109f19191b4c62509362bd64920cdf601a066", size = 41188, upload-time = "2026-04-16T02:58:30.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/ef/953c6395966388ab37cfb77493c441372201406a260d722e2fefdff134fb/flask_marshmallow-1.5.0-py3-none-any.whl", hash = "sha256:99951c77e5654111ed733811c6dc9310bfb4c3688c78a9e76f80b5ae0b2279a6", size = 12161, upload-time = "2026-04-16T02:58:29.709Z" }, +] + +[[package]] +name = "flask-migrate" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alembic" }, + { name = "flask" }, + { name = "flask-sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/47c7b3c93855ceffc2eabfa271782332942443321a07de193e4198f920cf/flask_migrate-4.1.0.tar.gz", hash = "sha256:1a336b06eb2c3ace005f5f2ded8641d534c18798d64061f6ff11f79e1434126d", size = 21965, upload-time = "2025-01-10T18:51:11.848Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/c4/3f329b23d769fe7628a5fc57ad36956f1fb7132cf8837be6da762b197327/Flask_Migrate-4.1.0-py3-none-any.whl", hash = "sha256:24d8051af161782e0743af1b04a152d007bad9772b2bca67b7ec1e8ceeb3910d", size = 21237, upload-time = "2025-01-10T18:51:09.527Z" }, +] + +[[package]] +name = "flask-principal" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "flask" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/c7/2531aca6ab7baa3774fde2dfc9c9dd6d5a42576a1013a93701bfdc402fdd/Flask-Principal-0.4.0.tar.gz", hash = "sha256:f5d6134b5caebfdbb86f32d56d18ee44b080876a27269560a96ea35f75c99453", size = 5452, upload-time = "2013-06-14T18:56:14.264Z" } + +[[package]] +name = "flask-security-too" +version = "5.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "email-validator" }, + { name = "flask" }, + { name = "flask-login" }, + { name = "flask-principal" }, + { name = "flask-wtf" }, + { name = "libpass" }, + { name = "markupsafe" }, + { name = "wtforms" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/67/8fe3ad73fd82609975cfea2c65e5e490b19ff8349e5653d5f5db1e013581/flask_security_too-5.8.0.tar.gz", hash = "sha256:4f72e5a7e6ee5eaafc20a89f2510407eefa0614cc695ac469d19f9dbaa4b6c43", size = 750314, upload-time = "2026-04-15T01:48:55.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a9/1dffc0358f24c7952adfecfadf1c5a577449c94e24fd64d01d90647bceb2/flask_security_too-5.8.0-py3-none-any.whl", hash = "sha256:f4bc8fb4b1c8a6f33bcd72ee698c9c2db0bc9406b1a211bb6d5dd6204bad6c53", size = 491804, upload-time = "2026-04-15T01:48:53.565Z" }, +] + +[package.optional-dependencies] +fsqla = [ + { name = "flask-sqlalchemy" }, + { name = "sqlalchemy" }, +] +mfa = [ + { name = "cryptography" }, + { name = "phonenumberslite" }, + { name = "qrcode" }, + { name = "webauthn" }, +] + +[[package]] +name = "flask-sqlalchemy" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/53/b0a9fcc1b1297f51e68b69ed3b7c3c40d8c45be1391d77ae198712914392/flask_sqlalchemy-3.1.1.tar.gz", hash = "sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312", size = 81899, upload-time = "2023-09-11T21:42:36.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/6a/89963a5c6ecf166e8be29e0d1bf6806051ee8fe6c82e232842e3aeac9204/flask_sqlalchemy-3.1.1-py3-none-any.whl", hash = "sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0", size = 25125, upload-time = "2023-09-11T21:42:34.514Z" }, +] + +[[package]] +name = "flask-sslify" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/98/54f2ffaf886d25eb1591cfb534c04cbf983236d657d58d180fd9ccbb5e7f/Flask-SSLify-0.1.5.tar.gz", hash = "sha256:d33e1d3c09cd95154176aa8a7319418e52129fc482dd56d8a8ad7c24500d543e", size = 3034, upload-time = "2015-04-02T20:25:24.407Z" } + +[[package]] +name = "flask-swagger-ui" +version = "5.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/79/8b651ab77494b02bfa13a88f636e9529d710e81c8f089a8c6de05a5daa2f/flask_swagger_ui-5.32.4.tar.gz", hash = "sha256:7808a16ab040e6f2867b6ce285991ff4a904fcbad576b2b17eabaddb136ffb6c", size = 613311, upload-time = "2026-04-20T09:34:34.61Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/d3/9d6d3883f71fbd5b78ef63acc06bf21a3735d326b69fdb6bdb158895adfe/flask_swagger_ui-5.32.4-py3-none-any.whl", hash = "sha256:0a31ce7162f17cde2f1feacfeb82b36d11a88227334e78c832b431e67de0875e", size = 618315, upload-time = "2026-04-20T09:34:32.887Z" }, +] + +[[package]] +name = "flask-wtf" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "itsdangerous" }, + { name = "wtforms" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/9b/f1cd6e41bbf874f3436368f2c7ee3216c1e82d666ff90d1d800e20eb1317/flask_wtf-1.2.2.tar.gz", hash = "sha256:79d2ee1e436cf570bccb7d916533fa18757a2f18c290accffab1b9a0b684666b", size = 42641, upload-time = "2024-10-24T07:18:58.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/19/354449145fbebb65e7c621235b6ad69bebcfaec2142481f044d0ddc5b5c5/flask_wtf-1.2.2-py3-none-any.whl", hash = "sha256:e93160c5c5b6b571cf99300b6e01b72f9a101027cab1579901f8b10c5daf0b70", size = 12779, upload-time = "2024-10-24T07:18:56.976Z" }, +] + +[[package]] +name = "flexcache" +version = "0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/b0/8a21e330561c65653d010ef112bf38f60890051d244ede197ddaa08e50c1/flexcache-0.3.tar.gz", hash = "sha256:18743bd5a0621bfe2cf8d519e4c3bfdf57a269c15d1ced3fb4b64e0ff4600656", size = 15816, upload-time = "2024-03-09T03:21:07.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/cd/c883e1a7c447479d6e13985565080e3fea88ab5a107c21684c813dba1875/flexcache-0.3-py3-none-any.whl", hash = "sha256:d43c9fea82336af6e0115e308d9d33a185390b8346a017564611f1466dcd2e32", size = 13263, upload-time = "2024-03-09T03:21:05.635Z" }, +] + +[[package]] +name = "flexmeasures" +version = "0.32.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "altair" }, + { name = "apispec" }, + { name = "apispec-oneofschema" }, + { name = "apispec-webframeworks" }, + { name = "argon2-cffi" }, + { name = "bcrypt" }, + { name = "click" }, + { name = "click-default-group" }, + { name = "cryptography" }, + { name = "email-validator" }, + { name = "flask" }, + { name = "flask-classful" }, + { name = "flask-cors" }, + { name = "flask-json" }, + { name = "flask-login" }, + { name = "flask-mail" }, + { name = "flask-marshmallow" }, + { name = "flask-migrate" }, + { name = "flask-security-too", extra = ["fsqla", "mfa"] }, + { name = "flask-sqlalchemy" }, + { name = "flask-sslify" }, + { name = "flask-swagger-ui" }, + { name = "flask-wtf" }, + { name = "highspy" }, + { name = "humanize" }, + { name = "inflect" }, + { name = "inflection" }, + { name = "iso8601" }, + { name = "isodate" }, + { name = "lightgbm" }, + { name = "marshmallow" }, + { name = "marshmallow-oneofschema" }, + { name = "marshmallow-polyfield" }, + { name = "marshmallow-sqlalchemy" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "pint" }, + { name = "psycopg2-binary" }, + { name = "py-moneyed" }, + { name = "pydantic" }, + { name = "pyomo" }, + { name = "pyopenssl" }, + { name = "python-dotenv" }, + { name = "pytz" }, + { name = "pyyaml" }, + { name = "redis" }, + { name = "rq" }, + { name = "rq-dashboard" }, + { name = "rq-win" }, + { name = "sentry-sdk", extra = ["flask"] }, + { name = "sqlalchemy" }, + { name = "tabulate" }, + { name = "timely-beliefs", extra = ["forecast"] }, + { name = "timetomodel" }, + { name = "tldextract" }, + { name = "u8darts" }, + { name = "uniplot" }, + { name = "urllib3" }, + { name = "vl-convert-python" }, + { name = "webargs" }, + { name = "werkzeug" }, + { name = "workalendar" }, + { name = "xlrd" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/00/e89562c7de0d96cb9620be5cbca7d4c004ab5544b7ccff23f50edcec5b24/flexmeasures-0.32.2.tar.gz", hash = "sha256:8888f63fc2b1ea2101784500d487e88f5e50bd74e2930a66d3ee6cfd247195c6", size = 2620032, upload-time = "2026-05-12T17:54:25.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/03/b1fc92c47b75af872c76eb4773e8bd9023998ae82fe171004b3786c362f0/flexmeasures-0.32.2-py3-none-any.whl", hash = "sha256:a5d0c4c4231db3ecec0031b65e46d98c632c2f3ce0eab30fea65dd229169f774", size = 2412646, upload-time = "2026-05-12T17:54:22.786Z" }, +] + +[[package]] +name = "flexmeasures-openadr3" +version = "0.1" +source = { editable = "." } +dependencies = [ + { name = "flexmeasures" }, + { name = "openadr3-client" }, +] + +[package.dev-dependencies] +dev = [ + { name = "fakeredis" }, + { name = "mypy" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "rq-dashboard" }, + { name = "ruff" }, + { name = "testcontainers", extra = ["keycloak"] }, + { name = "types-click" }, + { name = "types-docker" }, + { name = "types-flask" }, + { name = "types-python-dateutil" }, + { name = "types-pytz" }, + { name = "types-redis" }, + { name = "types-requests" }, + { name = "types-tzlocal" }, +] + +[package.metadata] +requires-dist = [ + { name = "flexmeasures", specifier = ">=0.32.0" }, + { name = "openadr3-client", specifier = ">=2.0.4" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "fakeredis", specifier = ">=2.35.1" }, + { name = "mypy", specifier = ">=1.0.0" }, + { name = "pre-commit", specifier = ">=3.0.0" }, + { name = "pytest", specifier = ">=7.0.0" }, + { name = "pytest-cov", specifier = ">=4.0.0" }, + { name = "rq-dashboard", specifier = ">=0.8.6" }, + { name = "ruff", specifier = ">=0.15.11" }, + { name = "testcontainers", extras = ["keycloak", "postgres"], specifier = ">=4.14.2" }, + { name = "types-click", specifier = ">=7.1.8" }, + { name = "types-docker", specifier = ">=7.1.0.20260518" }, + { name = "types-flask", specifier = ">=1.1.6" }, + { name = "types-python-dateutil", specifier = ">=2.9.0.20260408" }, + { name = "types-pytz", specifier = ">=2026.1.1.20260408" }, + { name = "types-redis", specifier = ">=4.6.0.20241004" }, + { name = "types-requests", specifier = ">=2.33.0.20260408" }, + { name = "types-tzlocal", specifier = ">=5.1.0.1" }, +] + +[[package]] +name = "flexparser" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/99/b4de7e39e8eaf8207ba1a8fa2241dd98b2ba72ae6e16960d8351736d8702/flexparser-0.4.tar.gz", hash = "sha256:266d98905595be2ccc5da964fe0a2c3526fbbffdc45b65b3146d75db992ef6b2", size = 31799, upload-time = "2024-11-07T02:00:56.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl", hash = "sha256:3738b456192dcb3e15620f324c447721023c0293f6af9955b481e91d00179846", size = 27625, upload-time = "2024-11-07T02:00:54.523Z" }, +] + +[[package]] +name = "fonttools" +version = "4.62.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974", size = 2870219, upload-time = "2026-03-13T13:52:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/66/9e/a769c8e99b81e5a87ab7e5e7236684de4e96246aae17274e5347d11ebd78/fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9", size = 2414891, upload-time = "2026-03-13T13:52:56.493Z" }, + { url = "https://files.pythonhosted.org/packages/69/64/f19a9e3911968c37e1e620e14dfc5778299e1474f72f4e57c5ec771d9489/fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936", size = 5033197, upload-time = "2026-03-13T13:52:59.179Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392", size = 4988768, upload-time = "2026-03-13T13:53:02.761Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/0f904540d3e6ab463c1243a0d803504826a11604c72dd58c2949796a1762/fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04", size = 4971512, upload-time = "2026-03-13T13:53:05.678Z" }, + { url = "https://files.pythonhosted.org/packages/29/0b/5cbef6588dc9bd6b5c9ad6a4d5a8ca384d0cea089da31711bbeb4f9654a6/fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d", size = 5122723, upload-time = "2026-03-13T13:53:08.662Z" }, + { url = "https://files.pythonhosted.org/packages/4a/47/b3a5342d381595ef439adec67848bed561ab7fdb1019fa522e82101b7d9c/fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c", size = 2281278, upload-time = "2026-03-13T13:53:10.998Z" }, + { url = "https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42", size = 2331414, upload-time = "2026-03-13T13:53:13.992Z" }, + { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155, upload-time = "2026-03-13T13:53:16.132Z" }, + { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802, upload-time = "2026-03-13T13:53:18.878Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926, upload-time = "2026-03-13T13:53:21.379Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575, upload-time = "2026-03-13T13:53:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693, upload-time = "2026-03-13T13:53:26.569Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" }, + { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" }, + { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" }, + { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" }, + { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" }, + { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" }, + { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" }, + { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" }, + { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" }, + { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" }, + { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, +] + +[[package]] +name = "greenlet" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/94/a5935717b307d7c71fe877b52b884c6af707d2d2090db118a03fbd799369/greenlet-3.4.0.tar.gz", hash = "sha256:f50a96b64dafd6169e595a5c56c9146ef80333e67d4476a65a9c55f400fc22ff", size = 195913, upload-time = "2026-04-08T17:08:00.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/8b/3669ad3b3f247a791b2b4aceb3aa5a31f5f6817bf547e4e1ff712338145a/greenlet-3.4.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:1a54a921561dd9518d31d2d3db4d7f80e589083063ab4d3e2e950756ef809e1a", size = 286902, upload-time = "2026-04-08T15:52:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/38/3e/3c0e19b82900873e2d8469b590a6c4b3dfd2b316d0591f1c26b38a4879a5/greenlet-3.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16dec271460a9a2b154e3b1c2fa1050ce6280878430320e85e08c166772e3f97", size = 606099, upload-time = "2026-04-08T16:24:38.408Z" }, + { url = "https://files.pythonhosted.org/packages/b5/33/99fef65e7754fc76a4ed14794074c38c9ed3394a5bd129d7f61b705f3168/greenlet-3.4.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90036ce224ed6fe75508c1907a77e4540176dcf0744473627785dd519c6f9996", size = 618837, upload-time = "2026-04-08T16:30:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/44/57/eae2cac10421feae6c0987e3dc106c6d86262b1cb379e171b017aba893a6/greenlet-3.4.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6f0def07ec9a71d72315cf26c061aceee53b306c36ed38c35caba952ea1b319d", size = 624901, upload-time = "2026-04-08T16:40:38.981Z" }, + { url = "https://files.pythonhosted.org/packages/36/f7/229f3aed6948faa20e0616a0b8568da22e365ede6a54d7d369058b128afd/greenlet-3.4.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1c4f6b453006efb8310affb2d132832e9bbb4fc01ce6df6b70d810d38f1f6dc", size = 615062, upload-time = "2026-04-08T15:56:33.766Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8a/0e73c9b94f31d1cc257fe79a0eff621674141cdae7d6d00f40de378a1e42/greenlet-3.4.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:0e1254cf0cbaa17b04320c3a78575f29f3c161ef38f59c977108f19ffddaf077", size = 423927, upload-time = "2026-04-08T16:43:05.293Z" }, + { url = "https://files.pythonhosted.org/packages/08/97/d988180011aa40135c46cd0d0cf01dd97f7162bae14139b4a3ef54889ba5/greenlet-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b2d9a138ffa0e306d0e2b72976d2fb10b97e690d40ab36a472acaab0838e2de", size = 1573511, upload-time = "2026-04-08T16:26:20.058Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0f/a5a26fe152fb3d12e6a474181f6e9848283504d0afd095f353d85726374b/greenlet-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8424683caf46eb0eb6f626cb95e008e8cc30d0cb675bdfa48200925c79b38a08", size = 1640396, upload-time = "2026-04-08T15:57:30.88Z" }, + { url = "https://files.pythonhosted.org/packages/42/cf/bb2c32d9a100e36ee9f6e38fad6b1e082b8184010cb06259b49e1266ca01/greenlet-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0a53fb071531d003b075c444014ff8f8b1a9898d36bb88abd9ac7b3524648a2", size = 238892, upload-time = "2026-04-08T17:03:10.094Z" }, + { url = "https://files.pythonhosted.org/packages/b7/47/6c41314bac56e71436ce551c7fbe3cc830ed857e6aa9708dbb9c65142eb6/greenlet-3.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:f38b81880ba28f232f1f675893a39cf7b6db25b31cc0a09bb50787ecf957e85e", size = 235599, upload-time = "2026-04-08T15:52:54.3Z" }, + { url = "https://files.pythonhosted.org/packages/7a/75/7e9cd1126a1e1f0cd67b0eda02e5221b28488d352684704a78ed505bd719/greenlet-3.4.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:43748988b097f9c6f09364f260741aa73c80747f63389824435c7a50bfdfd5c1", size = 285856, upload-time = "2026-04-08T15:52:45.82Z" }, + { url = "https://files.pythonhosted.org/packages/9d/c4/3e2df392e5cb199527c4d9dbcaa75c14edcc394b45040f0189f649631e3c/greenlet-3.4.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5566e4e2cd7a880e8c27618e3eab20f3494452d12fd5129edef7b2f7aa9a36d1", size = 610208, upload-time = "2026-04-08T16:24:39.674Z" }, + { url = "https://files.pythonhosted.org/packages/da/af/750cdfda1d1bd30a6c28080245be8d0346e669a98fdbae7f4102aa95fff3/greenlet-3.4.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1054c5a3c78e2ab599d452f23f7adafef55062a783a8e241d24f3b633ba6ff82", size = 621269, upload-time = "2026-04-08T16:30:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/e0/93/c8c508d68ba93232784bbc1b5474d92371f2897dfc6bc281b419f2e0d492/greenlet-3.4.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:98eedd1803353daf1cd9ef23eef23eda5a4d22f99b1f998d273a8b78b70dd47f", size = 628455, upload-time = "2026-04-08T16:40:40.698Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/0cbc693622cd54ebe25207efbb3a0eb07c2639cb8594f6e3aaaa0bb077a8/greenlet-3.4.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f82cb6cddc27dd81c96b1506f4aa7def15070c3b2a67d4e46fd19016aacce6cf", size = 617549, upload-time = "2026-04-08T15:56:34.893Z" }, + { url = "https://files.pythonhosted.org/packages/7f/46/cfaaa0ade435a60550fd83d07dfd5c41f873a01da17ede5c4cade0b9bab8/greenlet-3.4.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:b7857e2202aae67bc5725e0c1f6403c20a8ff46094ece015e7d474f5f7020b55", size = 426238, upload-time = "2026-04-08T16:43:06.865Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c0/8966767de01343c1ff47e8b855dc78e7d1a8ed2b7b9c83576a57e289f81d/greenlet-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:227a46251ecba4ff46ae742bc5ce95c91d5aceb4b02f885487aff269c127a729", size = 1575310, upload-time = "2026-04-08T16:26:21.671Z" }, + { url = "https://files.pythonhosted.org/packages/b8/38/bcdc71ba05e9a5fda87f63ffc2abcd1f15693b659346df994a48c968003d/greenlet-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5b99e87be7eba788dd5b75ba1cde5639edffdec5f91fe0d734a249535ec3408c", size = 1640435, upload-time = "2026-04-08T15:57:32.572Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c2/19b664b7173b9e4ef5f77e8cef9f14c20ec7fce7920dc1ccd7afd955d093/greenlet-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:849f8bc17acd6295fcb5de8e46d55cc0e52381c56eaf50a2afd258e97bc65940", size = 238760, upload-time = "2026-04-08T17:04:03.878Z" }, + { url = "https://files.pythonhosted.org/packages/9b/96/795619651d39c7fbd809a522f881aa6f0ead504cc8201c3a5b789dfaef99/greenlet-3.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:9390ad88b652b1903814eaabd629ca184db15e0eeb6fe8a390bbf8b9106ae15a", size = 235498, upload-time = "2026-04-08T17:05:00.584Z" }, + { url = "https://files.pythonhosted.org/packages/78/02/bde66806e8f169cf90b14d02c500c44cdbe02c8e224c9c67bafd1b8cadd1/greenlet-3.4.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:10a07aca6babdd18c16a3f4f8880acfffc2b88dfe431ad6aa5f5740759d7d75e", size = 286291, upload-time = "2026-04-08T17:09:34.307Z" }, + { url = "https://files.pythonhosted.org/packages/05/1f/39da1c336a87d47c58352fb8a78541ce63d63ae57c5b9dae1fe02801bbc2/greenlet-3.4.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:076e21040b3a917d3ce4ad68fb5c3c6b32f1405616c4a57aa83120979649bd3d", size = 656749, upload-time = "2026-04-08T16:24:41.721Z" }, + { url = "https://files.pythonhosted.org/packages/d3/6c/90ee29a4ee27af7aa2e2ec408799eeb69ee3fcc5abcecac6ddd07a5cd0f2/greenlet-3.4.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e82689eea4a237e530bb5cb41b180ef81fa2160e1f89422a67be7d90da67f615", size = 669084, upload-time = "2026-04-08T16:31:01.372Z" }, + { url = "https://files.pythonhosted.org/packages/d2/4a/74078d3936712cff6d3c91a930016f476ce4198d84e224fe6d81d3e02880/greenlet-3.4.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:06c2d3b89e0c62ba50bd7adf491b14f39da9e7e701647cb7b9ff4c99bee04b19", size = 673405, upload-time = "2026-04-08T16:40:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/07/49/d4cad6e5381a50947bb973d2f6cf6592621451b09368b8c20d9b8af49c5b/greenlet-3.4.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4df3b0b2289ec686d3c821a5fee44259c05cfe824dd5e6e12c8e5f5df23085cf", size = 665621, upload-time = "2026-04-08T15:56:35.995Z" }, + { url = "https://files.pythonhosted.org/packages/79/3e/df8a83ab894751bc31e1106fdfaa80ca9753222f106b04de93faaa55feb7/greenlet-3.4.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:070b8bac2ff3b4d9e0ff36a0d19e42103331d9737e8504747cd1e659f76297bd", size = 471670, upload-time = "2026-04-08T16:43:08.512Z" }, + { url = "https://files.pythonhosted.org/packages/37/31/d1edd54f424761b5d47718822f506b435b6aab2f3f93b465441143ea5119/greenlet-3.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8bff29d586ea415688f4cec96a591fcc3bf762d046a796cdadc1fdb6e7f2d5bf", size = 1622259, upload-time = "2026-04-08T16:26:23.201Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c6/6d3f9cdcb21c4e12a79cb332579f1c6aa1af78eb68059c5a957c7812d95e/greenlet-3.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a569c2fb840c53c13a2b8967c63621fafbd1a0e015b9c82f408c33d626a2fda", size = 1686916, upload-time = "2026-04-08T15:57:34.282Z" }, + { url = "https://files.pythonhosted.org/packages/63/45/c1ca4a1ad975de4727e52d3ffe641ae23e1d7a8ffaa8ff7a0477e1827b92/greenlet-3.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:207ba5b97ea8b0b60eb43ffcacf26969dd83726095161d676aac03ff913ee50d", size = 239821, upload-time = "2026-04-08T17:03:48.423Z" }, + { url = "https://files.pythonhosted.org/packages/71/c4/6f621023364d7e85a4769c014c8982f98053246d142420e0328980933ceb/greenlet-3.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:f8296d4e2b92af34ebde81085a01690f26a51eb9ac09a0fcadb331eb36dbc802", size = 236932, upload-time = "2026-04-08T17:04:33.551Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8f/18d72b629783f5e8d045a76f5325c1e938e659a9e4da79c7dcd10169a48d/greenlet-3.4.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d70012e51df2dbbccfaf63a40aaf9b40c8bed37c3e3a38751c926301ce538ece", size = 294681, upload-time = "2026-04-08T15:52:35.778Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ad/5fa86ec46769c4153820d58a04062285b3b9e10ba3d461ee257b68dcbf53/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a58bec0751f43068cd40cff31bb3ca02ad6000b3a51ca81367af4eb5abc480c8", size = 658899, upload-time = "2026-04-08T16:24:43.32Z" }, + { url = "https://files.pythonhosted.org/packages/43/f0/4e8174ca0e87ae748c409f055a1ba161038c43cc0a5a6f1433a26ac2e5bf/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05fa0803561028f4b2e3b490ee41216a842eaee11aed004cc343a996d9523aa2", size = 665284, upload-time = "2026-04-08T16:31:02.833Z" }, + { url = "https://files.pythonhosted.org/packages/ef/92/466b0d9afd44b8af623139a3599d651c7564fa4152f25f117e1ee5949ffb/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c4cd56a9eb7a6444edbc19062f7b6fbc8f287c663b946e3171d899693b1c19fa", size = 665872, upload-time = "2026-04-08T16:40:43.912Z" }, + { url = "https://files.pythonhosted.org/packages/19/da/991cf7cd33662e2df92a1274b7eb4d61769294d38a1bba8a45f31364845e/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e60d38719cb80b3ab5e85f9f1aed4960acfde09868af6762ccb27b260d68f4ed", size = 661861, upload-time = "2026-04-08T15:56:37.269Z" }, + { url = "https://files.pythonhosted.org/packages/0d/14/3395a7ef3e260de0325152ddfe19dffb3e49fe10873b94654352b53ad48e/greenlet-3.4.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:1f85f204c4d54134ae850d401fa435c89cd667d5ce9dc567571776b45941af72", size = 489237, upload-time = "2026-04-08T16:43:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/36/c5/6c2c708e14db3d9caea4b459d8464f58c32047451142fe2cfd90e7458f41/greenlet-3.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f50c804733b43eded05ae694691c9aa68bca7d0a867d67d4a3f514742a2d53f", size = 1622182, upload-time = "2026-04-08T16:26:24.777Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4c/50c5fed19378e11a29fabab1f6be39ea95358f4a0a07e115a51ca93385d8/greenlet-3.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2d4f0635dc4aa638cda4b2f5a07ae9a2cff9280327b581a3fcb6f317b4fbc38a", size = 1685050, upload-time = "2026-04-08T15:57:36.453Z" }, + { url = "https://files.pythonhosted.org/packages/db/72/85ae954d734703ab48e622c59d4ce35d77ce840c265814af9c078cacc7aa/greenlet-3.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1a4a48f24681300c640f143ba7c404270e1ebbbcf34331d7104a4ff40f8ea705", size = 245554, upload-time = "2026-04-08T17:03:50.044Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "highspy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/66/e74b1a805f65c52666e3b54cfc1ba783e745c2c8a7abaae9e7ef2d9e7270/highspy-1.14.0.tar.gz", hash = "sha256:b09cb5e3179a25fc615b8b0941130b0f71e19372c119f3dd620d63b54cd3ca4c", size = 1654913, upload-time = "2026-04-06T15:53:31.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/3d/83ee11de10ff6499efdb6edbb4586b472a4e9f982c0f6c5d3faa670bf1c3/highspy-1.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a8d0339887d65f5ef20c59be529af33115197d47f775ee21fca911a87d30f92", size = 2311831, upload-time = "2026-04-06T15:52:08.042Z" }, + { url = "https://files.pythonhosted.org/packages/b6/74/51cfca0c382886e302c4e6b9a50f9b160d214a88b4bc5937f5f8e2452dd9/highspy-1.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f61406a287b19680ece9a8c0bc3926e31d26ad2b4df8ba49f22666ce762fb7d", size = 2122237, upload-time = "2026-04-06T15:52:09.587Z" }, + { url = "https://files.pythonhosted.org/packages/78/69/a60f9dc033712f564089700441fb08c7f89db32bccab7926ef95db6b2306/highspy-1.14.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74ee022cc8cc0e3a576f9b2309287974ea80db68adf8c9f1c5698dc725ec0497", size = 2409165, upload-time = "2026-04-06T15:52:11.067Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c5/efa6d74704aa0bc5ffce9975553f6d13f4527e6fad8f79e7cacadfedd3d3/highspy-1.14.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb7c564ee426355671edf6ad17b2ece5aaa74f21b326ed5e7a8ba5bdc880f207", size = 2632243, upload-time = "2026-04-06T15:52:12.529Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1f/a701fee9ca318e6d175d719f8916d090dd7c8100c28bc591adac9fc2db35/highspy-1.14.0-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:44d80756dde11941336933d777ae85b913c303fe40c3b7eb55fae0fe62bbea08", size = 2792226, upload-time = "2026-04-06T15:52:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/b1db0018292e46d75d7aa7889f220d0fa0f2243a628e57790de80f1fc22f/highspy-1.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ba00a35b6c4b96eb2d20d75238c6046ecf82ac85a603706e5f588d28c3b3abf", size = 3465843, upload-time = "2026-04-06T15:52:15.692Z" }, + { url = "https://files.pythonhosted.org/packages/6c/41/64c4b290a5237e14fbc7e4812d110aecf457273bbade8b27ca9b9ea86c5b/highspy-1.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e991b5ac8af64d686f2a56fb59391b4bb81a50d647fe1e31a1b0ff34d9d2bb51", size = 4042641, upload-time = "2026-04-06T15:52:17.573Z" }, + { url = "https://files.pythonhosted.org/packages/30/3a/cff37994f2fd313467749bc9939e5baab4aef0210c79547471d6bbda5e81/highspy-1.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c10554bd0a37d4be126f24cbb7f5aca5a4252dd6e572b51b2433ad6d94ce7a9d", size = 3712946, upload-time = "2026-04-06T15:52:19.466Z" }, + { url = "https://files.pythonhosted.org/packages/6d/60/f328af00a9f05838e766aa7808dc733bb74a4fa79adcf5fdd665cfb8d7ed/highspy-1.14.0-cp312-cp312-win32.whl", hash = "sha256:7290540f0352192e43bdc790a59a82cee1f8029bd8d6b9ca20b54b651256bab4", size = 1955124, upload-time = "2026-04-06T15:52:20.965Z" }, + { url = "https://files.pythonhosted.org/packages/69/ea/0b47c49b6df4474c603b6a232278d5d6e6afddcb9da5044e06dfec579222/highspy-1.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0568d0fb514dc82776c3be1041988fe428c9df2be0ce98c1bff6382c0d2a5ba", size = 2323321, upload-time = "2026-04-06T15:52:22.418Z" }, + { url = "https://files.pythonhosted.org/packages/2d/2e/43b5d51852a8b06a079cc324ee877a91d2e87128ecd99905b45840b0fd3e/highspy-1.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:777ce930bc29a984826bc4e65ca7fec0407284a30877bc0d179dacc6bb7ee972", size = 2311865, upload-time = "2026-04-06T15:52:24.112Z" }, + { url = "https://files.pythonhosted.org/packages/1d/49/7e1a308163954faa3e91cbc0f73282a24d99b9cb6e7314c6f4266fee88d5/highspy-1.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:479be627bbdf7646dccf5621059b9416e6480c3ffa16998e6cc4de89c40a3716", size = 2122334, upload-time = "2026-04-06T15:52:25.938Z" }, + { url = "https://files.pythonhosted.org/packages/87/8c/8ae1a6f3f645deeaaf5522ebd47c61f7d856e8883dd5e7f51318e00ca287/highspy-1.14.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863a9363b624d0ef0a5a5bfcbe437bcd3891676c8b7c4d801462320b1387ef37", size = 2409256, upload-time = "2026-04-06T15:52:27.306Z" }, + { url = "https://files.pythonhosted.org/packages/7c/40/501586f760677501f3b2f19f0b7236515d06c08db29ccaae838a6f20c05d/highspy-1.14.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba290153919da6368e9144189060a496077a738522555e89403504d379e8d63f", size = 2632208, upload-time = "2026-04-06T15:52:29.122Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/a4137e3fd564fe2615093771463d66fa74cc4a2a94e6b68c15a0f8572218/highspy-1.14.0-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:bdc317bc2572a591cf9f0f2415d4b0879f9f0a47bdff3cda44f85a943941e64b", size = 2792091, upload-time = "2026-04-06T15:52:30.586Z" }, + { url = "https://files.pythonhosted.org/packages/af/91/aa3d6758212f4bb14c4c424053932a030ead226f8138da396a0bb6216d66/highspy-1.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:02a987e0020bf28757df9efbdfd6abde6d501b64294f8201301958095dae7979", size = 3466025, upload-time = "2026-04-06T15:52:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/64/10/25cd0fe6b0dfbae39bee2b7a6dfb91e29b1c78a338e537eb5fc8475ea03d/highspy-1.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:03c9b39a9ca8fe973eb89093ac386a8606ac37beb0e637b1a9255e31bfe87ef3", size = 4043109, upload-time = "2026-04-06T15:52:34.203Z" }, + { url = "https://files.pythonhosted.org/packages/8c/27/4dbcba90d2c8b2ddcd3157d62f5531c2bfa75a3e360b8f05ac52996b489b/highspy-1.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a92b50e7508b90c6d65b315b7dd3b37c6d0db331e698052f7054da523e446eb4", size = 3712794, upload-time = "2026-04-06T15:52:36.163Z" }, + { url = "https://files.pythonhosted.org/packages/de/5a/72d9840b26b0b26916bd7624d606f31a0ce83f14709fcfaf52df31e29898/highspy-1.14.0-cp313-cp313-win32.whl", hash = "sha256:e8eb72be8766717ec856cf5fa3fcadd8ddd59bee0f4d71d4f45ae0a2b861795f", size = 1955044, upload-time = "2026-04-06T15:52:37.532Z" }, + { url = "https://files.pythonhosted.org/packages/22/7e/89f07a03ff9f5460043ba4815583fd3fda22aa6d088be3fb730346ccae63/highspy-1.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:3d15faaab62b408320373540bfd5a7b9a48e9a01072b847f2270b8d5d881647c", size = 2323514, upload-time = "2026-04-06T15:52:38.939Z" }, + { url = "https://files.pythonhosted.org/packages/27/d4/2658ccfef1c31e25a29e337c9ede3107394fad0a9535820a096cd50ad055/highspy-1.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cc9dfde9ad829f3463627dab84152ceb4c30c08b89b19226bf8f69d47a7fed5d", size = 2317451, upload-time = "2026-04-06T15:52:40.659Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f8/7d9be61c80a6daa782bf51b4324bf8425896bf120809ca804ad08f69d12e/highspy-1.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fc8ad7a6fc0a44c99b9aa3a3d0c3a917b50dfa8c61e332cb2ee1f7ea4c234e4f", size = 2121732, upload-time = "2026-04-06T15:52:42.189Z" }, + { url = "https://files.pythonhosted.org/packages/01/eb/47f960ccb56986c2c9ef4ce9f293bae95de7c22a662f6c45b844c316d7ff/highspy-1.14.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39339a7a000998ab26eb87add814222fcdc304d38cfba2f6e098189b53ef0a79", size = 2410647, upload-time = "2026-04-06T15:52:43.674Z" }, + { url = "https://files.pythonhosted.org/packages/2d/38/3b37047686105955e2d54ec753c3b9e9d135bdd8e5ee1df310a87be25472/highspy-1.14.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c8f31f2635517c7d370be612a1e9102481483c749b9db786760dd489fad22519", size = 2632928, upload-time = "2026-04-06T15:52:45.373Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/2a6673db80a5388f364d0f7218af08aebbfc05c866d1b4cc2ec79bc4d822/highspy-1.14.0-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:68b944014ce307e24921c3bf904253d8849799ded819bda39a4b09359074ea0a", size = 2791407, upload-time = "2026-04-06T15:52:47.051Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/75f862b96420dd77a5f88d6401212534833b99aa0ef971d4eddfd227f913/highspy-1.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:03538f68dff2038582d8aa7f5d05690ce459435f498ea0022869fe962d70d8ae", size = 3471297, upload-time = "2026-04-06T15:52:48.892Z" }, + { url = "https://files.pythonhosted.org/packages/ed/17/564c24dcc05d6f11876485654956dfb6b2f4ba17f21ecafe84b059a17ab4/highspy-1.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a54d687522347348639a62df270f45546a3cbd84a6cd230dc41732e9559c766f", size = 4045124, upload-time = "2026-04-06T15:52:50.383Z" }, + { url = "https://files.pythonhosted.org/packages/b1/34/a611fe3271be165fb13a7b85970820579515d652bd59f69aed001ed2ff98/highspy-1.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:88db4d1ebefb119991ff16adb624c682ebc20fc586c86156843cb4be7c965d8a", size = 3713436, upload-time = "2026-04-06T15:52:51.849Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0d/001726678facdd7ca435d430bf039732cc50d77fdbd6231fd3bac428893b/highspy-1.14.0-cp314-cp314-win32.whl", hash = "sha256:7a85730676ffc88eadca1721252bec168f6ffc0423f6141f6ad41f79bb441327", size = 1999958, upload-time = "2026-04-06T15:52:54.154Z" }, + { url = "https://files.pythonhosted.org/packages/a4/4d/c7f2b5c23c4b7103095a9959add5119c5653c17c6bc7817fd003bd3ba8c6/highspy-1.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:90b7074d4bc34a4390636aaf9e4232ae15d4536098f1f1f39f04329c08751148", size = 2410786, upload-time = "2026-04-06T15:52:56.161Z" }, +] + +[[package]] +name = "holidays" +version = "0.95" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/ab/003a4edf31234a3bc92b74b7d8ef18d1c1f019ed02dedd440d5c05f367cc/holidays-0.95.tar.gz", hash = "sha256:3b79fdd141cbc740526a5184d1cd41fd264fac4aba383554359d4bb6d52937d3", size = 887562, upload-time = "2026-04-20T20:26:10.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/e2/3a605baf0025b8ea819eea3ec4ca4ecb4d8c371975c1ca4bebd9979fae4c/holidays-0.95-py3-none-any.whl", hash = "sha256:5a204ff6d2ac2fff82fb5ca2a55dbbf265c2f952ca225c16b9ce12755664256d", size = 1453540, upload-time = "2026-04-20T20:26:08.425Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "humanize" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/66/a3921783d54be8a6870ac4ccffcd15c4dc0dd7fcce51c6d63b8c63935276/humanize-4.15.0.tar.gz", hash = "sha256:1dd098483eb1c7ee8e32eb2e99ad1910baefa4b75c3aff3a82f4d78688993b10", size = 83599, upload-time = "2025-12-20T20:16:13.19Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/7b/bca5613a0c3b542420cf92bd5e5fb8ebd5435ce1011a091f66bb7693285e/humanize-4.15.0-py3-none-any.whl", hash = "sha256:b1186eb9f5a9749cd9cb8565aee77919dd7c8d076161cf44d70e59e3301e1769", size = 132203, upload-time = "2025-12-20T20:16:11.67Z" }, +] + +[[package]] +name = "identify" +version = "2.6.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, +] + +[[package]] +name = "idna" +version = "3.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/12/2948fbe5513d062169bd91f7d7b1cd97bc8894f32946b71fa39f6e63ca0c/idna-3.12.tar.gz", hash = "sha256:724e9952cc9e2bd7550ea784adb098d837ab5267ef67a1ab9cf7846bdbdd8254", size = 194350, upload-time = "2026-04-21T13:32:48.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/b2/acc33950394b3becb2b664741a0c0889c7ef9f9ffbfa8d47eddb53a50abd/idna-3.12-py3-none-any.whl", hash = "sha256:60ffaa1858fac94c9c124728c24fcde8160f3fb4a7f79aa8cdd33a9d1af60a67", size = 68634, upload-time = "2026-04-21T13:32:47.403Z" }, +] + +[[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 = "inflect" +version = "7.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, + { name = "typeguard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/c6/943357d44a21fd995723d07ccaddd78023eace03c1846049a2645d4324a3/inflect-7.5.0.tar.gz", hash = "sha256:faf19801c3742ed5a05a8ce388e0d8fe1a07f8d095c82201eb904f5d27ad571f", size = 73751, upload-time = "2024-12-28T17:11:18.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/eb/427ed2b20a38a4ee29f24dbe4ae2dafab198674fe9a85e3d6adf9e5f5f41/inflect-7.5.0-py3-none-any.whl", hash = "sha256:2aea70e5e70c35d8350b8097396ec155ffd68def678c7ff97f51aa69c1d92344", size = 35197, upload-time = "2024-12-28T17:11:15.931Z" }, +] + +[[package]] +name = "inflection" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/7e/691d061b7329bc8d54edbf0ec22fbfb2afe61facb681f9aaa9bff7a27d04/inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", size = 15091, upload-time = "2020-08-22T08:16:29.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454, upload-time = "2020-08-22T08:16:27.816Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "iso8601" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/f3/ef59cee614d5e0accf6fd0cbba025b93b272e626ca89fb70a3e9187c5d15/iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df", size = 6522, upload-time = "2023-10-03T00:25:39.317Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/0c/f37b6a241f0759b7653ffa7213889d89ad49a2b76eb2ddf3b57b2738c347/iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242", size = 7545, upload-time = "2023-10-03T00:25:32.304Z" }, +] + +[[package]] +name = "isodate" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +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 = "jwcrypto" +version = "1.5.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/90/f065668004d22715c1940d6e88e4c3afc8ee16d5664e4478d2c8fd23a250/jwcrypto-1.5.7.tar.gz", hash = "sha256:70204d7cca406eda8c82352e3c41ba2d946610dafd19e54403f0a1f4f18633c6", size = 89535, upload-time = "2026-04-07T00:35:36.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/24/fb7da4d6613de7001feaf540d4b5969c6b5a1c42839043b0196cb13aa057/jwcrypto-1.5.7-py3-none-any.whl", hash = "sha256:729463fefe28b6de5cf1ebfda3e94f1a1b41d2799148ef98a01cb9678ebe2bb0", size = 94799, upload-time = "2026-04-07T00:35:35.085Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, + { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, + { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, + { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, + { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, + { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, + { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, + { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, + { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, + { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, + { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, +] + +[[package]] +name = "libpass" +version = "1.9.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/f6/210563eef148773ef835d21c0fa1e8a43c5b10fceb1f63d5d650635b81da/libpass-1.9.3.tar.gz", hash = "sha256:7830b9323d9ba96a841ad698a8dec1d43a2b0b7f1c855c76772e7972c1c6d959", size = 692901, upload-time = "2025-10-09T14:26:28.363Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/a6/ff00d926a9f4ece9e0732bcea1c3e8e9280390f48cec53f237e21f220082/libpass-1.9.3-py3-none-any.whl", hash = "sha256:3cbeb14d22086660e92c30d787ea981973d67394e81c8c870e1a83cfe1c84353", size = 307118, upload-time = "2025-10-09T14:26:25.551Z" }, +] + +[[package]] +name = "librt" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/6b/3d5c13fb3e3c4f43206c8f9dfed13778c2ed4f000bacaa0b7ce3c402a265/librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d", size = 184368, upload-time = "2026-04-09T16:06:26.173Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/90/89ddba8e1c20b0922783cd93ed8e64f34dc05ab59c38a9c7e313632e20ff/librt-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4", size = 68332, upload-time = "2026-04-09T16:05:00.09Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/7aa4da1fb08bdeeb540cb07bfc8207cb32c5c41642f2594dbd0098a0662d/librt-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d", size = 70581, upload-time = "2026-04-09T16:05:01.213Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/73a2187e1031041e93b7e3a25aae37aa6f13b838c550f7e0f06f66766212/librt-0.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f", size = 203984, upload-time = "2026-04-09T16:05:02.542Z" }, + { url = "https://files.pythonhosted.org/packages/5e/3d/23460d571e9cbddb405b017681df04c142fb1b04cbfce77c54b08e28b108/librt-0.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27", size = 215762, upload-time = "2026-04-09T16:05:04.127Z" }, + { url = "https://files.pythonhosted.org/packages/de/1e/42dc7f8ab63e65b20640d058e63e97fd3e482c1edbda3570d813b4d0b927/librt-0.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2", size = 230288, upload-time = "2026-04-09T16:05:05.883Z" }, + { url = "https://files.pythonhosted.org/packages/dc/08/ca812b6d8259ad9ece703397f8ad5c03af5b5fedfce64279693d3ce4087c/librt-0.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b", size = 224103, upload-time = "2026-04-09T16:05:07.148Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3f/620490fb2fa66ffd44e7f900254bc110ebec8dac6c1b7514d64662570e6f/librt-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265", size = 232122, upload-time = "2026-04-09T16:05:08.386Z" }, + { url = "https://files.pythonhosted.org/packages/e9/83/12864700a1b6a8be458cf5d05db209b0d8e94ae281e7ec261dbe616597b4/librt-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084", size = 225045, upload-time = "2026-04-09T16:05:09.707Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1b/845d339c29dc7dbc87a2e992a1ba8d28d25d0e0372f9a0a2ecebde298186/librt-0.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8", size = 227372, upload-time = "2026-04-09T16:05:10.942Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fe/277985610269d926a64c606f761d58d3db67b956dbbf40024921e95e7fcb/librt-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f", size = 248224, upload-time = "2026-04-09T16:05:12.254Z" }, + { url = "https://files.pythonhosted.org/packages/92/1b/ee486d244b8de6b8b5dbaefabe6bfdd4a72e08f6353edf7d16d27114da8d/librt-0.9.0-cp312-cp312-win32.whl", hash = "sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f", size = 55986, upload-time = "2026-04-09T16:05:13.529Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/ba1737012308c17dc6d5516143b5dce9a2c7ba3474afd54e11f44a4d1ef3/librt-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745", size = 63260, upload-time = "2026-04-09T16:05:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/e4/01752c113da15127f18f7bf11142f5640038f062407a611c059d0036c6aa/librt-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9", size = 53694, upload-time = "2026-04-09T16:05:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d7/1b3e26fffde1452d82f5666164858a81c26ebe808e7ae8c9c88628981540/librt-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e", size = 68367, upload-time = "2026-04-09T16:05:17.243Z" }, + { url = "https://files.pythonhosted.org/packages/a5/5b/c61b043ad2e091fbe1f2d35d14795e545d0b56b03edaa390fa1dcee3d160/librt-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22", size = 70595, upload-time = "2026-04-09T16:05:18.471Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/2448471196d8a73370aa2f23445455dc42712c21404081fcd7a03b9e0749/librt-0.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a", size = 204354, upload-time = "2026-04-09T16:05:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5e/39fc4b153c78cfd2c8a2dcb32700f2d41d2312aa1050513183be4540930d/librt-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5", size = 216238, upload-time = "2026-04-09T16:05:20.868Z" }, + { url = "https://files.pythonhosted.org/packages/d7/42/bc2d02d0fa7badfa63aa8d6dcd8793a9f7ef5a94396801684a51ed8d8287/librt-0.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11", size = 230589, upload-time = "2026-04-09T16:05:22.305Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7b/e2d95cc513866373692aa5edf98080d5602dd07cabfb9e5d2f70df2f25f7/librt-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858", size = 224610, upload-time = "2026-04-09T16:05:23.647Z" }, + { url = "https://files.pythonhosted.org/packages/31/d5/6cec4607e998eaba57564d06a1295c21b0a0c8de76e4e74d699e627bd98c/librt-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e", size = 232558, upload-time = "2026-04-09T16:05:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/95/8c/27f1d8d3aaf079d3eb26439bf0b32f1482340c3552e324f7db9dca858671/librt-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0", size = 225521, upload-time = "2026-04-09T16:05:26.311Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d8/1e0d43b1c329b416017619469b3c3801a25a6a4ef4a1c68332aeaa6f72ca/librt-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2", size = 227789, upload-time = "2026-04-09T16:05:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b4/d3d842e88610fcd4c8eec7067b0c23ef2d7d3bff31496eded6a83b0f99be/librt-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d", size = 248616, upload-time = "2026-04-09T16:05:29.181Z" }, + { url = "https://files.pythonhosted.org/packages/ec/28/527df8ad0d1eb6c8bdfa82fc190f1f7c4cca5a1b6d7b36aeabf95b52d74d/librt-0.9.0-cp313-cp313-win32.whl", hash = "sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd", size = 56039, upload-time = "2026-04-09T16:05:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a7/413652ad0d92273ee5e30c000fc494b361171177c83e57c060ecd3c21538/librt-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519", size = 63264, upload-time = "2026-04-09T16:05:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0a/92c244309b774e290ddb15e93363846ae7aa753d9586b8aad511c5e6145b/librt-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5", size = 53728, upload-time = "2026-04-09T16:05:33.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c1/184e539543f06ea2912f4b92a5ffaede4f9b392689e3f00acbf8134bee92/librt-0.9.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb", size = 67830, upload-time = "2026-04-09T16:05:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/23399bdcb7afca819acacdef31b37ee59de261bd66b503a7995c03c4b0dc/librt-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499", size = 70280, upload-time = "2026-04-09T16:05:35.649Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0b/4542dc5a2b8772dbf92cafb9194701230157e73c14b017b6961a23598b03/librt-0.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f", size = 201925, upload-time = "2026-04-09T16:05:36.739Z" }, + { url = "https://files.pythonhosted.org/packages/31/d4/8ee7358b08fd0cfce051ef96695380f09b3c2c11b77c9bfbc367c921cce5/librt-0.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1", size = 212381, upload-time = "2026-04-09T16:05:38.043Z" }, + { url = "https://files.pythonhosted.org/packages/f2/94/a2025fe442abedf8b038038dab3dba942009ad42b38ea064a1a9e6094241/librt-0.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f", size = 227065, upload-time = "2026-04-09T16:05:39.394Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e9/b9fcf6afa909f957cfbbf918802f9dada1bd5d3c1da43d722fd6a310dc3f/librt-0.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a", size = 221333, upload-time = "2026-04-09T16:05:40.999Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7c/ba54cd6aa6a3c8cd12757a6870e0c79a64b1e6327f5248dcff98423f4d43/librt-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f", size = 229051, upload-time = "2026-04-09T16:05:42.605Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4b/8cfdbad314c8677a0148bf0b70591d6d18587f9884d930276098a235461b/librt-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845", size = 222492, upload-time = "2026-04-09T16:05:43.842Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d1/2eda69563a1a88706808decdce035e4b32755dbfbb0d05e1a65db9547ed1/librt-0.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b", size = 223849, upload-time = "2026-04-09T16:05:45.054Z" }, + { url = "https://files.pythonhosted.org/packages/04/44/b2ed37df6be5b3d42cfe36318e0598e80843d5c6308dd63d0bf4e0ce5028/librt-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b", size = 245001, upload-time = "2026-04-09T16:05:46.34Z" }, + { url = "https://files.pythonhosted.org/packages/47/e7/617e412426df89169dd2a9ed0cc8752d5763336252c65dbf945199915119/librt-0.9.0-cp314-cp314-win32.whl", hash = "sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9", size = 51799, upload-time = "2026-04-09T16:05:47.738Z" }, + { url = "https://files.pythonhosted.org/packages/24/ed/c22ca4db0ca3cbc285e4d9206108746beda561a9792289c3c31281d7e9df/librt-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e", size = 59165, upload-time = "2026-04-09T16:05:49.198Z" }, + { url = "https://files.pythonhosted.org/packages/24/56/875398fafa4cbc8f15b89366fc3287304ddd3314d861f182a4b87595ace0/librt-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f", size = 49292, upload-time = "2026-04-09T16:05:50.362Z" }, + { url = "https://files.pythonhosted.org/packages/4c/61/bc448ecbf9b2d69c5cff88fe41496b19ab2a1cbda0065e47d4d0d51c0867/librt-0.9.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4", size = 70175, upload-time = "2026-04-09T16:05:51.564Z" }, + { url = "https://files.pythonhosted.org/packages/60/f2/c47bb71069a73e2f04e70acbd196c1e5cc411578ac99039a224b98920fd4/librt-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228", size = 72951, upload-time = "2026-04-09T16:05:52.699Z" }, + { url = "https://files.pythonhosted.org/packages/29/19/0549df59060631732df758e8886d92088da5fdbedb35b80e4643664e8412/librt-0.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54", size = 225864, upload-time = "2026-04-09T16:05:53.895Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f8/3b144396d302ac08e50f89e64452c38db84bc7b23f6c60479c5d3abd303c/librt-0.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71", size = 241155, upload-time = "2026-04-09T16:05:55.191Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ce/ee67ec14581de4043e61d05786d2aed6c9b5338816b7859bcf07455c6a9f/librt-0.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938", size = 252235, upload-time = "2026-04-09T16:05:56.549Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fa/0ead15daa2b293a54101550b08d4bafe387b7d4a9fc6d2b985602bae69b6/librt-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3", size = 244963, upload-time = "2026-04-09T16:05:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/29/68/9fbf9a9aa704ba87689e40017e720aced8d9a4d2b46b82451d8142f91ec9/librt-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283", size = 257364, upload-time = "2026-04-09T16:05:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8d/9d60869f1b6716c762e45f66ed945b1e5dd649f7377684c3b176ae424648/librt-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee", size = 247661, upload-time = "2026-04-09T16:06:00.938Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/a5c365093962310bfdb4f6af256f191085078ffb529b3f0cbebb5b33ebe2/librt-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c", size = 248238, upload-time = "2026-04-09T16:06:02.537Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3c/2d34365177f412c9e19c0a29f969d70f5343f27634b76b765a54d8b27705/librt-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15", size = 269457, upload-time = "2026-04-09T16:06:03.833Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/de45b239ea3bdf626f982a00c14bfcf2e12d261c510ba7db62c5969a27cd/librt-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40", size = 52453, upload-time = "2026-04-09T16:06:05.229Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f9/bfb32ae428aa75c0c533915622176f0a17d6da7b72b5a3c6363685914f70/librt-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118", size = 60044, upload-time = "2026-04-09T16:06:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/aa/47/7d70414bcdbb3bc1f458a8d10558f00bbfdb24e5a11740fc8197e12c3255/librt-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61", size = 50009, upload-time = "2026-04-09T16:06:07.995Z" }, +] + +[[package]] +name = "lightgbm" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/0b/a2e9f5c5da7ef047cc60cef37f86185088845e8433e54d2e7ed439cce8a3/lightgbm-4.6.0.tar.gz", hash = "sha256:cb1c59720eb569389c0ba74d14f52351b573af489f230032a1c9f314f8bab7fe", size = 1703705, upload-time = "2025-02-15T04:03:03.111Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/75/cffc9962cca296bc5536896b7e65b4a7cdeb8db208e71b9c0133c08f8f7e/lightgbm-4.6.0-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:b7a393de8a334d5c8e490df91270f0763f83f959574d504c7ccb9eee4aef70ed", size = 2010151, upload-time = "2025-02-15T04:02:50.961Z" }, + { url = "https://files.pythonhosted.org/packages/21/1b/550ee378512b78847930f5d74228ca1fdba2a7fbdeaac9aeccc085b0e257/lightgbm-4.6.0-py3-none-macosx_12_0_arm64.whl", hash = "sha256:2dafd98d4e02b844ceb0b61450a660681076b1ea6c7adb8c566dfd66832aafad", size = 1592172, upload-time = "2025-02-15T04:02:53.937Z" }, + { url = "https://files.pythonhosted.org/packages/64/41/4fbde2c3d29e25ee7c41d87df2f2e5eda65b431ee154d4d462c31041846c/lightgbm-4.6.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4d68712bbd2b57a0b14390cbf9376c1d5ed773fa2e71e099cac588703b590336", size = 3454567, upload-time = "2025-02-15T04:02:56.443Z" }, + { url = "https://files.pythonhosted.org/packages/42/86/dabda8fbcb1b00bcfb0003c3776e8ade1aa7b413dff0a2c08f457dace22f/lightgbm-4.6.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:cb19b5afea55b5b61cbb2131095f50538bd608a00655f23ad5d25ae3e3bf1c8d", size = 3569831, upload-time = "2025-02-15T04:02:58.925Z" }, + { url = "https://files.pythonhosted.org/packages/5e/23/f8b28ca248bb629b9e08f877dd2965d1994e1674a03d67cd10c5246da248/lightgbm-4.6.0-py3-none-win_amd64.whl", hash = "sha256:37089ee95664b6550a7189d887dbf098e3eadab03537e411f52c63c121e3ba4b", size = 1451509, upload-time = "2025-02-15T04:03:01.515Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.47.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz", hash = "sha256:62031ce968ec74e95092184d4b0e857e444f8fdff0b8f9213707699570c33ccc", size = 193614, upload-time = "2026-03-31T18:29:53.497Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/48/4b7fe0e34c169fa2f12532916133e0b219d2823b540733651b34fdac509a/llvmlite-0.47.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:306a265f408c259067257a732c8e159284334018b4083a9e35f67d19792b164f", size = 37232769, upload-time = "2026-03-31T18:28:43.735Z" }, + { url = "https://files.pythonhosted.org/packages/e6/4b/e3f2cd17822cf772a4a51a0a8080b0032e6d37b2dbe8cfb724eac4e31c52/llvmlite-0.47.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5853bf26160857c0c2573415ff4efe01c4c651e59e2c55c2a088740acfee51cd", size = 56275178, upload-time = "2026-03-31T18:28:48.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a3b4a543185305a9bdf3d9759d53646ed96e55e7dfd43f53e7a421b8fbae/llvmlite-0.47.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:003bcf7fa579e14db59c1a1e113f93ab8a06b56a4be31c7f08264d1d4072d077", size = 55128632, upload-time = "2026-03-31T18:28:52.901Z" }, + { url = "https://files.pythonhosted.org/packages/2f/f5/d281ae0f79378a5a91f308ea9fdb9f9cc068fddd09629edc0725a5a8fde1/llvmlite-0.47.0-cp312-cp312-win_amd64.whl", hash = "sha256:f3079f25bdc24cd9d27c4b2b5e68f5f60c4fdb7e8ad5ee2b9b006007558f9df7", size = 38138692, upload-time = "2026-03-31T18:28:57.147Z" }, + { url = "https://files.pythonhosted.org/packages/77/6f/4615353e016799f80fa52ccb270a843c413b22361fadda2589b2922fb9b0/llvmlite-0.47.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a3c6a735d4e1041808434f9d440faa3d78d9b4af2ee64d05a66f351883b6ceec", size = 37232771, upload-time = "2026-03-31T18:29:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/31/b8/69f5565f1a280d032525878a86511eebed0645818492feeb169dfb20ae8e/llvmlite-0.47.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2699a74321189e812d476a43d6d7f652f51811e7b5aad9d9bba842a1c7927acb", size = 56275178, upload-time = "2026-03-31T18:29:05.748Z" }, + { url = "https://files.pythonhosted.org/packages/d6/da/b32cafcb926fb0ce2aa25553bf32cb8764af31438f40e2481df08884c947/llvmlite-0.47.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c6951e2b29930227963e53ee152441f0e14be92e9d4231852102d986c761e40", size = 55128632, upload-time = "2026-03-31T18:29:11.235Z" }, + { url = "https://files.pythonhosted.org/packages/46/9f/4898b44e4042c60fafcb1162dfb7014f6f15b1ec19bf29cfea6bf26df90d/llvmlite-0.47.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2e9adf8698d813a9a5efb2d4370caf344dbc1e145019851fee6a6f319ba760e", size = 38138695, upload-time = "2026-03-31T18:29:15.43Z" }, + { url = "https://files.pythonhosted.org/packages/1c/d4/33c8af00f0bf6f552d74f3a054f648af2c5bc6bece97972f3bfadce4f5ec/llvmlite-0.47.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:de966c626c35c9dff5ae7bf12db25637738d0df83fc370cf793bc94d43d92d14", size = 37232773, upload-time = "2026-03-31T18:29:19.453Z" }, + { url = "https://files.pythonhosted.org/packages/64/1d/a760e993e0c0ba6db38d46b9f48f6c7dceb8ac838824997fb9e25f97bc04/llvmlite-0.47.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ddbccff2aeaff8670368340a158abefc032fe9b3ccf7d9c496639263d00151aa", size = 56275176, upload-time = "2026-03-31T18:29:24.149Z" }, + { url = "https://files.pythonhosted.org/packages/84/3b/e679bc3b29127182a7f4aa2d2e9e5bea42adb93fb840484147d59c236299/llvmlite-0.47.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4a7b778a2e144fc64468fb9bf509ac1226c9813a00b4d7afea5d988c4e22fca", size = 55128631, upload-time = "2026-03-31T18:29:29.536Z" }, + { url = "https://files.pythonhosted.org/packages/be/f7/19e2a09c62809c9e63bbd14ce71fb92c6ff7b7b3045741bb00c781efc3c9/llvmlite-0.47.0-cp314-cp314-win_amd64.whl", hash = "sha256:694e3c2cdc472ed2bd8bd4555ca002eec4310961dd58ef791d508f57b5cc4c94", size = 39153826, upload-time = "2026-03-31T18:29:33.681Z" }, + { url = "https://files.pythonhosted.org/packages/40/a1/581a8c707b5e80efdbbe1dd94527404d33fe50bceb71f39d5a7e11bd57b7/llvmlite-0.47.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:92ec8a169a20b473c1c54d4695e371bde36489fc1efa3688e11e99beba0abf9c", size = 37232772, upload-time = "2026-03-31T18:29:37.952Z" }, + { url = "https://files.pythonhosted.org/packages/11/03/16090dd6f74ba2b8b922276047f15962fbeea0a75d5601607edb301ba945/llvmlite-0.47.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa1cbd800edd3b20bc141521f7fd45a6185a5b84109aa6855134e81397ffe72b", size = 56275178, upload-time = "2026-03-31T18:29:42.58Z" }, + { url = "https://files.pythonhosted.org/packages/f5/cb/0abf1dd4c5286a95ffe0c1d8c67aec06b515894a0dd2ac97f5e27b82ab0b/llvmlite-0.47.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6725179b89f03b17dabe236ff3422cb8291b4c1bf40af152826dfd34e350ae8", size = 55128632, upload-time = "2026-03-31T18:29:46.939Z" }, + { url = "https://files.pythonhosted.org/packages/4f/79/d3bbab197e86e0ff4f9c07122895b66a3e0d024247fcff7f12c473cb36d9/llvmlite-0.47.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6842cf6f707ec4be3d985a385ad03f72b2d724439e118fcbe99b2929964f0453", size = 39153839, upload-time = "2026-03-31T18:29:51.004Z" }, +] + +[[package]] +name = "lunardate" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/71/62fadf243502618cf2a7af5b81936a99684147c655729934f014758c3b39/lunardate-0.2.2.tar.gz", hash = "sha256:8ff8c01721bef7710f0712a302b1cb7d0b8b3ffc72a63a9a056cd01e58e5485a", size = 17686, upload-time = "2023-12-07T13:13:21.09Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/07/232401bb1c6bc699c5e641ad375e2582cdde90b19b18e6618ec51796b737/lunardate-0.2.2-py3-none-any.whl", hash = "sha256:cf1916337e50470a82df12d885ff47a456e89c91a3ad4e5fdf1575e063a7ed55", size = 18081, upload-time = "2023-12-07T13:13:19.333Z" }, +] + +[[package]] +name = "mako" +version = "1.3.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/8a/805404d0c0b9f3d7a326475ca008db57aea9c5c9f2e1e39ed0faa335571c/mako-1.3.11.tar.gz", hash = "sha256:071eb4ab4c5010443152255d77db7faa6ce5916f35226eb02dc34479b6858069", size = 399811, upload-time = "2026-04-14T20:19:51.493Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/a5/19d7aaa7e433713ffe881df33705925a196afb9532efc8475d26593921a6/mako-1.3.11-py3-none-any.whl", hash = "sha256:e372c6e333cf004aa736a15f425087ec977e1fcbd2966aae7f17c8dc1da27a77", size = 78503, upload-time = "2026-04-14T20:19:53.233Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "marshmallow" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/7e/1dbd4096eb7c148cd2841841916f78820bb85a4d80a0c25c02d30815a7fb/marshmallow-4.3.0.tar.gz", hash = "sha256:fb43c53b3fe240b8f6af37223d6ef1636f927ad9bea8ab323afad95dff090880", size = 224485, upload-time = "2026-04-03T21:46:32.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/e0/ff24e25218bb59eb6290a530cea40651b14068b6e3659b20f9c175179632/marshmallow-4.3.0-py3-none-any.whl", hash = "sha256:46c4fe6984707e3cbd485dfebbf0a59874f58d695aad05c1668d15e8c6e13b46", size = 49148, upload-time = "2026-04-03T21:46:31.241Z" }, +] + +[[package]] +name = "marshmallow-oneofschema" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "marshmallow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/42/a0e00dea6a831acfe9d3fe664d695b7cefc02c27dd69d9ccb4bdc3c3d1a7/marshmallow_oneofschema-3.2.0.tar.gz", hash = "sha256:c06c8d9f14d51ffff152d66d85bd5f27d55cff10752a3b1f8c1f948bf5f597a0", size = 9096, upload-time = "2025-05-08T13:49:34.798Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/15/52d6ac14dcfe381e4f1c204c9c287623b8b462bc27c6cc468dba0560ed4c/marshmallow_oneofschema-3.2.0-py3-none-any.whl", hash = "sha256:19c87e6124ef05e2831e5c631168c909a50a8fe399921b9841b75fef3785be8c", size = 5898, upload-time = "2025-05-08T13:49:33.26Z" }, +] + +[[package]] +name = "marshmallow-polyfield" +version = "5.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "marshmallow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/55/b752d034a09afe34d680c65cf8634c0c55f9b57e7a673774bc13ca44b53e/marshmallow-polyfield-5.11.tar.gz", hash = "sha256:8075a9cc490da4af58b902b4a40a99882dd031adb7aaa96abd147a4fcd53415f", size = 8596, upload-time = "2022-10-26T03:10:40.661Z" } + +[[package]] +name = "marshmallow-sqlalchemy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "marshmallow" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/fe/247c297809e64116f766716632adbc3f4cd06f376f56dc15bb92f170d247/marshmallow_sqlalchemy-1.5.0.tar.gz", hash = "sha256:e51192c204770645a2fab0d72f44f8789272eef75951f84b1608d6b4b0bfe0e6", size = 51349, upload-time = "2026-04-01T23:21:03.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/b7/407c44dbd77a7670b7be6b7fedbe5329348fe00b4e62dae0f5c83a9aeafa/marshmallow_sqlalchemy-1.5.0-py3-none-any.whl", hash = "sha256:3865232672f3dd38c4d5e4e85fdedce76904200742c3594948a2d11d0af93258", size = 16582, upload-time = "2026-04-01T23:21:02.376Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, + { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, + { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, + { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, + { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, +] + +[[package]] +name = "more-itertools" +version = "11.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/f7/139d22fef48ac78127d18e01d80cf1be40236ae489769d17f35c3d425293/more_itertools-11.0.2.tar.gz", hash = "sha256:392a9e1e362cbc106a2457d37cabf9b36e5e12efd4ebff1654630e76597df804", size = 144659, upload-time = "2026-04-09T15:01:33.297Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.2-py3-none-any.whl", hash = "sha256:6e35b35f818b01f691643c6c611bc0902f2e92b46c18fffa77ae1e7c46e912e4", size = 71939, upload-time = "2026-04-09T15:01:32.21Z" }, +] + +[[package]] +name = "mypy" +version = "1.20.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/af/e3d4b3e9ec91a0ff9aabfdb38692952acf49bbb899c2e4c29acb3a6da3ae/mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665", size = 3817349, upload-time = "2026-04-21T17:12:28.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/4e/7560e4528db9e9b147e4c0f22660466bf30a0a1fe3d63d1b9d3b0fd354ee/mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b", size = 14539393, upload-time = "2026-04-21T17:07:12.52Z" }, + { url = "https://files.pythonhosted.org/packages/32/d9/34a5efed8124f5a9234f55ac6a4ced4201e2c5b81e1109c49ad23190ec8c/mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4", size = 13361642, upload-time = "2026-04-21T17:06:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/d1/14/eb377acf78c03c92d566a1510cda8137348215b5335085ef662ab82ecd3a/mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6", size = 13740347, upload-time = "2026-04-21T17:12:04.73Z" }, + { url = "https://files.pythonhosted.org/packages/b9/94/7e4634a32b641aa1c112422eed1bbece61ee16205f674190e8b536f884de/mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066", size = 14734042, upload-time = "2026-04-21T17:07:43.16Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f3/f7e62395cb7f434541b4491a01149a4439e28ace4c0c632bbf5431e92d1f/mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102", size = 14964958, upload-time = "2026-04-21T17:11:00.665Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0d/47e3c3a0ec2a876e35aeac365df3cac7776c36bbd4ed18cc521e1b9d255b/mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9", size = 10911340, upload-time = "2026-04-21T17:10:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b2/6c852d72e0ea8b01f49da817fb52539993cde327e7d010e0103dc12d0dac/mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58", size = 9833947, upload-time = "2026-04-21T17:09:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c4/b93812d3a192c9bcf5df405bd2f30277cd0e48106a14d1023c7f6ed6e39b/mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026", size = 14524670, upload-time = "2026-04-21T17:10:30.737Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/42c122501bff18eaf1e8f457f5c017933452d8acdc52918a9f59f6812955/mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943", size = 13336218, upload-time = "2026-04-21T17:08:44.069Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/75bbc92f41725fbd585fb17b440b1119b576105df1013622983e18640a93/mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517", size = 13724906, upload-time = "2026-04-21T17:08:01.02Z" }, + { url = "https://files.pythonhosted.org/packages/a1/32/4c49da27a606167391ff0c39aa955707a00edc500572e562f7c36c08a71f/mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15", size = 14726046, upload-time = "2026-04-21T17:11:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/7f/fc/4e354a1bd70216359deb0c9c54847ee6b32ef78dfb09f5131ff99b494078/mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee", size = 14955587, upload-time = "2026-04-21T17:12:16.033Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/c0f2056e9eb8f08c62cafd9715e4584b89132bdc832fcf85d27d07b5f3e5/mypy-1.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f", size = 10922681, upload-time = "2026-04-21T17:06:35.842Z" }, + { url = "https://files.pythonhosted.org/packages/e5/14/065e333721f05de8ef683d0aa804c23026bcc287446b61cac657b902ccac/mypy-1.20.2-cp313-cp313-win_arm64.whl", hash = "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330", size = 9830560, upload-time = "2026-04-21T17:07:51.023Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d1/b4ec96b0ecc620a4443570c6e95c867903428cfcde4206518eafdd5880c3/mypy-1.20.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30", size = 14524561, upload-time = "2026-04-21T17:06:27.325Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/d2c2ff4fa66bc49477d32dfa26e8a167ba803ea6a69c5efb416036909d30/mypy-1.20.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924", size = 13363883, upload-time = "2026-04-21T17:11:11.239Z" }, + { url = "https://files.pythonhosted.org/packages/2a/56/983916806bf4eddeaaa2c9230903c3669c6718552a921154e1c5182c701f/mypy-1.20.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb", size = 13742945, upload-time = "2026-04-21T17:08:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/19/65/0cd9285ab010ee8214c83d67c6b49417c40d86ce46f1aa109457b5a9b8d7/mypy-1.20.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc", size = 14706163, upload-time = "2026-04-21T17:05:15.51Z" }, + { url = "https://files.pythonhosted.org/packages/94/97/48ff3b297cafcc94d185243a9190836fb1b01c1b0918fff64e941e973cc9/mypy-1.20.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558", size = 14938677, upload-time = "2026-04-21T17:05:39.562Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a1/1b4233d255bdd0b38a1f284feeb1c143ca508c19184964e22f8d837ec851/mypy-1.20.2-cp314-cp314-win_amd64.whl", hash = "sha256:913485a03f1bcf5d279409a9d2b9ed565c151f61c09f29991e5faa14033da4c8", size = 11089322, upload-time = "2026-04-21T17:06:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/ce7ee2ba36aeb954ba50f18fa25d9c1188578654b97d02a66a15b6f09531/mypy-1.20.2-cp314-cp314-win_arm64.whl", hash = "sha256:c3bae4f855d965b5453784300c12ffc63a548304ac7f99e55d4dc7c898673aa3", size = 10017775, upload-time = "2026-04-21T17:07:20.732Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a1/9d93a7d0b5859af0ead82b4888b46df6c8797e1bc5e1e262a08518c6d48e/mypy-1.20.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609", size = 15549002, upload-time = "2026-04-21T17:08:23.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/d2/09a6a10ee1bf0008f6c144d9676f2ca6a12512151b4e0ad0ff6c4fac5337/mypy-1.20.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2", size = 14401942, upload-time = "2026-04-21T17:07:31.837Z" }, + { url = "https://files.pythonhosted.org/packages/57/da/9594b75c3c019e805250bed3583bdf4443ff9e6ef08f97e39ae308cb06f2/mypy-1.20.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c", size = 15041649, upload-time = "2026-04-21T17:09:34.653Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/f75a65c278e6e8eba2071f7f5a90481891053ecc39878cc444634d892abe/mypy-1.20.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744", size = 15864588, upload-time = "2026-04-21T17:11:44.936Z" }, + { url = "https://files.pythonhosted.org/packages/d7/46/1a4e1c66e96c1a3246ddf5403d122ac9b0a8d2b7e65730b9d6533ba7a6d3/mypy-1.20.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6", size = 16093956, upload-time = "2026-04-21T17:10:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2c/78a8851264dec38cd736ca5b8bc9380674df0dd0be7792f538916157716c/mypy-1.20.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9bcb8aa397ff0093c824182fd76a935a9ba7ad097fcbef80ae89bf6c1731d8ec", size = 12568661, upload-time = "2026-04-21T17:11:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/83/01/cd7318aa03493322ce275a0e14f4f52b8896335e4e79d4fb8153a7ad2b77/mypy-1.20.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e061b58443f1736f8a37c48978d7ab581636d6ab03e3d4f99e3fa90463bb9382", size = 10389240, upload-time = "2026-04-21T17:09:42.719Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/f23c163e25b11074188251b0b5a0342625fc1cdb6af604757174fa9acc9b/mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563", size = 2637314, upload-time = "2026-04-21T17:05:54.5Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "narwhals" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/f3/257adc69a71011b4c8cda321b00f02c5bf1980ae38ffd05a58d9632d4de8/narwhals-2.20.0.tar.gz", hash = "sha256:c10994975fa7dc5a68c2cffcddbd5908fc8ebb2d463c5bab085309c0ee1f551e", size = 627848, upload-time = "2026-04-20T12:11:45.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl", hash = "sha256:16e750ea5507d4ba6e8d03455b5f93a535e0405976561baea235bca5dc9f475d", size = 449373, upload-time = "2026-04-20T12:11:43.596Z" }, +] + +[[package]] +name = "nfoursid" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/36/1c0543d9c5ca3a692ea5244de68a74776e38e49b77b6ddb1ec3e8e28beca/nfoursid-1.0.2.tar.gz", hash = "sha256:d65ba1bb98715f6310d0136aecdc997313caa239c08cc95f7e48378f77d06a24", size = 15475, upload-time = "2025-07-24T13:01:40.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/36/fae16163620cbf04de0e50f67d946782ff4964e65276852dceea8a3c655a/nfoursid-1.0.2-py3-none-any.whl", hash = "sha256:2b51f319346a65bd2a66554d9bfd7846feb9ad359fad678566dfa3f124eace70", size = 18439, upload-time = "2025-07-24T13:01:39.132Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "numba" +version = "0.65.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/61/7299643b9c18d669e04be7c5bcb64d985070d07553274817b45b049e7bfe/numba-0.65.0.tar.gz", hash = "sha256:edad0d9f6682e93624c00125a471ae4df186175d71fd604c983c377cdc03e68b", size = 2764131, upload-time = "2026-04-01T03:52:01.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/2f/8bd31a1ea43c01ac215283d83aa5f8d5acbe7a36c85b82f1757bfe9ccb31/numba-0.65.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:b27ee4847e1bfb17e9604d100417ee7c1d10f15a6711c6213404b3da13a0b2aa", size = 2680705, upload-time = "2026-04-01T03:51:32.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/36/88406bd58600cc696417b8e5dd6a056478da808f3eaf48d18e2421e0c2d9/numba-0.65.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a52d92ffd297c10364bce60cd1fcb88f99284ab5df085f2c6bcd1cb33b529a6f", size = 3801411, upload-time = "2026-04-01T03:51:34.321Z" }, + { url = "https://files.pythonhosted.org/packages/0c/61/ce753a1d7646dd477e16d15e89473703faebb8995d2f71d7ad69a540b565/numba-0.65.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da8e371e328c06d0010c3d8b44b21858652831b85bcfba78cb22c042e22dbd8e", size = 3501622, upload-time = "2026-04-01T03:51:36.348Z" }, + { url = "https://files.pythonhosted.org/packages/7d/86/db87a5393f1b1fabef53ac3ba4e6b938bb27e40a04ad7cc512098fcae032/numba-0.65.0-cp312-cp312-win_amd64.whl", hash = "sha256:59bb9f2bb9f1238dfd8e927ba50645c18ae769fef4f3d58ea0ea22a2683b91f5", size = 2749979, upload-time = "2026-04-01T03:51:37.88Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/eee0f1ff456218db036bfc9023995ec1f85a9dc8f2422f1594f6a87829e0/numba-0.65.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:c6334094563a456a695c812e6846288376ca02327cf246cdcc83e1bb27862367", size = 2680679, upload-time = "2026-04-01T03:51:39.491Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8f/3d116e4b8e92f6abace431afa4b2b944f4d65bdee83af886f5c4b263df95/numba-0.65.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b8a9008411615c69d083d1dcf477f75a5aa727b30beb16e139799e2be945cdfd", size = 3809537, upload-time = "2026-04-01T03:51:41.42Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/6a3ca4128e253cb67affe06deb47688f51ce968f5111e2a06d010e6f1fa6/numba-0.65.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af96c0cba53664efcb361528b8c75e011a6556c859c7e08424c2715201c6cf7a", size = 3508615, upload-time = "2026-04-01T03:51:43.444Z" }, + { url = "https://files.pythonhosted.org/packages/96/0e/267f9a36fb282c104a971d7eecb685b411c47dce2a740fe69cf5fc2945d9/numba-0.65.0-cp313-cp313-win_amd64.whl", hash = "sha256:6254e73b9c929dc736a1fbd3d6f5680789709a5067cae1fa7198707385129c04", size = 2749938, upload-time = "2026-04-01T03:51:45.218Z" }, + { url = "https://files.pythonhosted.org/packages/56/a4/90edb01e9176053578e343d7a7276bc28356741ee67059aed8ed2c1a4e59/numba-0.65.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:ee336b398a6fca51b1f626034de99f50cb1bd87d537a166275158a3cee744b82", size = 2680878, upload-time = "2026-04-01T03:51:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/24/8d/e12d6ff4b9119db3cbf7b2db1ce257576441bd3c76388c786dea74f20b02/numba-0.65.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:05c0a9fdf75d85f57dee47b719e8d6415707b80aae45d75f63f9dc1b935c29f7", size = 3778456, upload-time = "2026-04-01T03:51:48.552Z" }, + { url = "https://files.pythonhosted.org/packages/17/89/abcd83e76f6a773276fe76244140671bcc5bf820f6e2ae1a15362ae4c8c9/numba-0.65.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:583680e0e8faf124d362df23b4b593f3221a8996341a63d1b664c122401bec2f", size = 3478464, upload-time = "2026-04-01T03:51:50.527Z" }, + { url = "https://files.pythonhosted.org/packages/73/5b/fbce55ce3d933afbc7ade04df826853e4a846aaa47d58d2fbb669b8f2d08/numba-0.65.0-cp314-cp314-win_amd64.whl", hash = "sha256:add297d3e1c08dd884f44100152612fa41e66a51d15fdf91307f9dde31d06830", size = 2752012, upload-time = "2026-04-01T03:51:52.691Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ab/af705f4257d9388fb2fd6d7416573e98b6ca9c786e8b58f02720978557bd/numba-0.65.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:194a243ba53a9157c8538cbb3166ec015d785a8c5d584d06cdd88bee902233c7", size = 2683961, upload-time = "2026-04-01T03:51:54.281Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e5/8267b0adb0c01b52b553df5062fbbb42c30ed5362d08b85cc913a36f838f/numba-0.65.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c7fa502960f7a2f3f5cb025bc7bff888a3551277b92431bfdc5ba2f11a375749", size = 3816373, upload-time = "2026-04-01T03:51:56.18Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f5/b8397ca360971669a93706b9274592b6864e4367a37d498fbbcb62aa2d48/numba-0.65.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5046c63f783ca3eb6195f826a50797465e7c4ce811daa17c9bea47e310c9b964", size = 3532782, upload-time = "2026-04-01T03:51:58.387Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1e73fa16bf0393ebb74c5bb208d712152ffdfc84600a8e93a3180317856e/numba-0.65.0-cp314-cp314t-win_amd64.whl", hash = "sha256:46fd679ae4f68c7a5d5721efbd29ecee0b0f3013211591891d79b51bfdf73113", size = 2757611, upload-time = "2026-04-01T03:52:00.083Z" }, +] + +[[package]] +name = "numpy" +version = "1.26.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, + { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" }, + { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" }, + { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + +[package.optional-dependencies] +signedtoken = [ + { name = "cryptography" }, + { name = "pyjwt" }, +] + +[[package]] +name = "openadr3-client" +version = "2.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib", extra = ["signedtoken"] }, + { name = "pycountry" }, + { name = "pydantic" }, + { name = "pydantic-extra-types" }, + { name = "requests" }, + { name = "requests-oauthlib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/9b/1187ed5d6d0db5fcb40a0a7fa5087755c9378a18aba033aad0251f67dfa5/openadr3_client-2.0.4.tar.gz", hash = "sha256:8671e96a152a25b19bb7b46bf955356376aef7c085393bab33fc2c45e95509d5", size = 63033, upload-time = "2026-03-16T10:01:37.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/3b/ee4fd55d343097d0fd8f27b590c936d57a31d78daa6e0da02641162334a3/openadr3_client-2.0.4-py3-none-any.whl", hash = "sha256:369de74ac26acbaf0732a2046d566ed5a3da692af807d388235ed2a240e6e945", size = 139814, upload-time = "2026-03-16T10:01:35.937Z" }, +] + +[[package]] +name = "openturns" +version = "1.26" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, + { name = "psutil" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/cd/490782a207902d18e1b825e735300b4886f54dc3cc389a8855f834de4434/openturns-1.26-cp39-abi3-macosx_14_0_arm64.whl", hash = "sha256:f51acf4b2eb39dc16c1719af06980cdad0cd1b0d21dfe0a86353e6f685fe60c3", size = 58397268, upload-time = "2025-11-24T17:02:48.061Z" }, + { url = "https://files.pythonhosted.org/packages/4d/5e/3fd6149e8b161c3f3d7cc03e2a3d6b4a77770fbfeb2a44c2457abc163cb4/openturns-1.26-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:31a68b6968e08bc2bd2df56487387f37d6b3964fc564fc1ec0d7f91f942fb588", size = 62556348, upload-time = "2025-11-24T17:02:01.097Z" }, + { url = "https://files.pythonhosted.org/packages/e3/77/ddef997d1e242504b79475b750a2f49a9fc89ce28408a12e1cd60def0749/openturns-1.26-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa46c2708ca79659c0601ca387c630dfb8e6b94ef17eed7d0d004eaac4471ddc", size = 66816452, upload-time = "2025-11-24T17:04:23.87Z" }, + { url = "https://files.pythonhosted.org/packages/16/22/02f7c226115fc1d0fcd52e635a4c27aaa4a5c39e75afb4175fc6bddf9665/openturns-1.26-cp39-abi3-win_amd64.whl", hash = "sha256:bf1e6c1532c74669ad3a4f7fc37e82a19f1b7799db11e2bb33b6f30b77d1ec6c", size = 65365313, upload-time = "2025-11-24T17:09:18.131Z" }, +] + +[[package]] +name = "packaging" +version = "26.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, +] + +[[package]] +name = "pandas" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/59/2afa81b9fb300c90531803c0fd43ff4548074fa3e8d0f747ef63b3b5e77a/pandas-2.2.1.tar.gz", hash = "sha256:0ab90f87093c13f3e8fa45b48ba9f39181046e8f3317d3aadb2fffbb1b978572", size = 4395256, upload-time = "2024-02-23T15:32:10.41Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/b9/660353ce2b1bd5b6e0f5c992836d91909c0da1ccb59c16565ad0a37e839d/pandas-2.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:88ecb5c01bb9ca927ebc4098136038519aa5d66b44671861ffab754cae75102c", size = 12493183, upload-time = "2024-02-23T15:31:14.786Z" }, + { url = "https://files.pythonhosted.org/packages/19/4e/6a7f400d4b65f82e37eefa7dbbe3e6f0a4fa542ca7ebb68c787eeebdc497/pandas-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:04f6ec3baec203c13e3f8b139fb0f9f86cd8c0b94603ae3ae8ce9a422e9f5bee", size = 11335860, upload-time = "2024-02-23T15:31:18.461Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2b/3e00e92a6b430313da68b15e925c6dba05f672d716cf3b02bcd3d0381974/pandas-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a935a90a76c44fe170d01e90a3594beef9e9a6220021acfb26053d01426f7dc2", size = 15189183, upload-time = "2024-02-23T15:31:22.396Z" }, + { url = "https://files.pythonhosted.org/packages/78/f4/19f1dda9ab1eaa38301e445925f92b303d415d4c4115e56c0d62774421f7/pandas-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c391f594aae2fd9f679d419e9a4d5ba4bce5bb13f6a989195656e7dc4b95c8f0", size = 12742656, upload-time = "2024-02-23T15:31:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cd/8b84912b5bfab19b1fcea2f732d2e3a2d134d558f141e9dffa5dbfd9d23b/pandas-2.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9d1265545f579edf3f8f0cb6f89f234f5e44ba725a34d86535b1a1d38decbccc", size = 15861331, upload-time = "2024-02-23T15:31:29.742Z" }, + { url = "https://files.pythonhosted.org/packages/11/e7/65bf50aff86da6554cdffdcd87ced857c79a29dfaf1d85fdf97955d76d02/pandas-2.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11940e9e3056576ac3244baef2fedade891977bcc1cb7e5cc8f8cc7d603edc89", size = 13410754, upload-time = "2024-02-23T15:31:33.62Z" }, + { url = "https://files.pythonhosted.org/packages/71/00/6beaeeba7f075d15ea167a5caa039b861e58ff2f58a5b659abb9b544c8f6/pandas-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acf681325ee1c7f950d058b05a820441075b0dd9a2adf5c4835b9bc056bf4fb", size = 11478767, upload-time = "2024-02-23T15:31:37.563Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "patsy" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/44/ed13eccdd0519eff265f44b670d46fbb0ec813e2274932dc1c0e48520f7d/patsy-1.0.2.tar.gz", hash = "sha256:cdc995455f6233e90e22de72c37fcadb344e7586fb83f06696f54d92f8ce74c0", size = 399942, upload-time = "2025-10-20T16:17:37.535Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/70/ba4b949bdc0490ab78d545459acd7702b211dfccf7eb89bbc1060f52818d/patsy-1.0.2-py2.py3-none-any.whl", hash = "sha256:37bfddbc58fcf0362febb5f54f10743f8b21dd2aa73dec7e7ef59d1b02ae668a", size = 233301, upload-time = "2025-10-20T16:17:36.563Z" }, +] + +[[package]] +name = "phonenumberslite" +version = "9.0.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/a6/fd7553f283e420525bdf85ee32e540f2c2c33ae241491ac351b3aa05fe41/phonenumberslite-9.0.28.tar.gz", hash = "sha256:ce6cedcdcac214f4bdee0cb88df5c49c020435c82a2f22c7a536887e061a1129", size = 288809, upload-time = "2026-04-13T14:15:54.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/6c/363ead32abc5a0abc660072e80f788c09b16e94274b5e7afe9868a9591d8/phonenumberslite-9.0.28-py2.py3-none-any.whl", hash = "sha256:ed6d00d17a0e3583101dd4060ae4eac0dc9bf8bcb0daaa7fd3a55d5d36806ff2", size = 473357, upload-time = "2026-04-13T14:15:52.182Z" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, +] + +[[package]] +name = "pint" +version = "0.25.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flexcache" }, + { name = "flexparser" }, + { name = "platformdirs" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/9d/b1379cdbd33a49d17d627bc24e2b63cca06a1c5343b38072d2889499e82e/pint-0.25.3.tar.gz", hash = "sha256:f8f5df6cf65314d74da1ade1bf96f8e3e4d0c41b51577ac53c49e7d44ca5acee", size = 255106, upload-time = "2026-03-19T21:57:08.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/dd/a9fe6a0a09512da23951c68bf36466aeecd89def3183dc095edbc807ddc5/pint-0.25.3-py3-none-any.whl", hash = "sha256:27eb25143bd5de9fcc4d5a4b484f16faf6b4615aa93ece6b3373a8c1a3c1b97d", size = 307488, upload-time = "2026-03-19T21:57:07.022Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "ply" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/69/882ee5c9d017149285cab114ebeab373308ef0f874fcdac9beb90e0ac4da/ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3", size = 159130, upload-time = "2018-02-15T19:01:31.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce", size = 49567, upload-time = "2018-02-15T19:01:27.172Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, +] + +[[package]] +name = "properscoring" +version = "0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/ac/513d2c8653ab6bc66c4502372e6e4e20ce6a136cde4c1ba9908ec36e34c1/properscoring-0.1.tar.gz", hash = "sha256:b0cc4963cc218b728d6c5f77b3259c8f835ae00e32e82678cdf6936049b93961", size = 17848, upload-time = "2015-11-12T19:54:29.615Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/ff/51706ba1a029d0f2df0322543793d3bf1383de9dc567d23886144cb21bef/properscoring-0.1-py2.py3-none-any.whl", hash = "sha256:f84d5b06c13549d0171ce52ad7b45c6f5726ac44b733d24af5c60654cbb821dc", size = 23427, upload-time = "2015-11-12T19:54:24.578Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/9f/ef4ef3c8e15083df90ca35265cfd1a081a2f0cc07bb229c6314c6af817f4/psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", size = 3712459, upload-time = "2026-04-20T23:34:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/b5/01/3dd14e46ba48c1e1a6ec58ee599fa1b5efa00c246d5046cd903d0eeb1af1/psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", size = 3822936, upload-time = "2026-04-20T23:34:32.77Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f7/0640e4901119d8a9f7a1784b927f494e2198e213ceb593753d1f2c8b1b30/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", size = 4578676, upload-time = "2026-04-20T23:34:35.18Z" }, + { url = "https://files.pythonhosted.org/packages/b0/55/44df3965b5f297c50cc0b1b594a31c67d6127a9d133045b8a66611b14dfb/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", size = 4274917, upload-time = "2026-04-20T23:34:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4b/74535248b1eac0c9336862e8617c765ac94dac76f9e25d7c4a79588c8907/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", size = 5894843, upload-time = "2026-04-20T23:34:40.856Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ba/f1bf8d2ae71868ad800b661099086ee52bc0f8d9f05be1acd8ebb06757cc/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", size = 4110556, upload-time = "2026-04-20T23:34:44.016Z" }, + { url = "https://files.pythonhosted.org/packages/45/46/c15706c338403b7c420bcc0c2905aad116cc064545686d8bf85f1999ea00/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", size = 3655714, upload-time = "2026-04-20T23:34:46.233Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7c/a2d5dc09b64a4564db242a0fe418fde7d33f6f8259dd2c5b9d7def00fb5a/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", size = 3301154, upload-time = "2026-04-20T23:34:49.528Z" }, + { url = "https://files.pythonhosted.org/packages/c0/e8/cc8c9a4ce71461f9ec548d38cadc41dc184b34c73e6455450775a9334ccd/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", size = 3048882, upload-time = "2026-04-20T23:34:51.86Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/31e2296bc0787c5ab75d3d118e40b239db8151b5192b90b77c72bc9256e9/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", size = 3351298, upload-time = "2026-04-20T23:34:54.124Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a8/75f4e3e11203b590150abed2cf7794b9c9c9f7eceddae955191138b44dde/psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", size = 2757230, upload-time = "2026-04-20T23:34:56.242Z" }, + { url = "https://files.pythonhosted.org/packages/91/bb/4608c96f970f6e0c56572e87027ef4404f709382a3503e9934526d7ba051/psycopg2_binary-2.9.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", size = 3712419, upload-time = "2026-04-20T23:34:58.754Z" }, + { url = "https://files.pythonhosted.org/packages/5e/af/48f76af9d50d61cf390f8cd657b503168b089e2e9298e48465d029fcc713/psycopg2_binary-2.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", size = 3822990, upload-time = "2026-04-20T23:35:00.821Z" }, + { url = "https://files.pythonhosted.org/packages/7a/df/aba0f99397cd811d32e06fc0cc781f1f3ce98bc0e729cb423925085d781a/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", size = 4578696, upload-time = "2026-04-20T23:35:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/eaa74021ac4e4d5c2f83d82fc6615a63f4fe6c94dc4e94c3990427053f67/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", size = 4274982, upload-time = "2026-04-20T23:35:05.583Z" }, + { url = "https://files.pythonhosted.org/packages/35/ed/c25deff98bd26187ba48b3b250a3ffc3037c46c5b89362534a15d200e0db/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", size = 5894867, upload-time = "2026-04-20T23:35:07.902Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/8d0e21ca77373c6c9589e5c4528f6e8f0c08c62cafc76fb0bddb7a2cee22/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", size = 4110578, upload-time = "2026-04-20T23:35:10.149Z" }, + { url = "https://files.pythonhosted.org/packages/00/fc/f481e2435bd8f742d0123309174aae4165160ad3ef17c1b99c3622c241d2/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", size = 3655816, upload-time = "2026-04-20T23:35:12.56Z" }, + { url = "https://files.pythonhosted.org/packages/53/79/b9f46466bdbe9f239c96cde8be33c1aace4842f06013b47b730dc9759187/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", size = 3301307, upload-time = "2026-04-20T23:35:15.029Z" }, + { url = "https://files.pythonhosted.org/packages/3f/19/7dc003b32fe35024df89b658104f7c8538a8b2dcbde7a4e746ce929742e7/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", size = 3048968, upload-time = "2026-04-20T23:35:16.757Z" }, + { url = "https://files.pythonhosted.org/packages/91/58/2dbd7db5c604d45f4950d988506aae672a14126ec22998ced5021cbb76bb/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", size = 3351369, upload-time = "2026-04-20T23:35:18.933Z" }, + { url = "https://files.pythonhosted.org/packages/42/ee/dee8dcaad07f735824de3d6563bc67119fa6c28257b17977a8d624f02fab/psycopg2_binary-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", size = 2757347, upload-time = "2026-04-20T23:35:21.283Z" }, + { url = "https://files.pythonhosted.org/packages/13/1b/708c0dca874acfad6d65314271859899a79007686f3a1f74e82a2ed4b645/psycopg2_binary-2.9.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", size = 3712428, upload-time = "2026-04-20T23:35:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/d6/39/ddbea9d4b4de6aca9431b6ed253f530f8a02d3b8f9bcfd0dbfe2b3de6fe4/psycopg2_binary-2.9.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", size = 3823184, upload-time = "2026-04-20T23:35:25.92Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a0/bc2fef74b106fa345567122a0659e6d94512ed7dc0131ec44c9e5aba3725/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", size = 4579157, upload-time = "2026-04-20T23:35:28.542Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/d4e3b2005d3de607ca4fbb0e8742e248056e52184a6b94ebda3c1c2c329b/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", size = 4274970, upload-time = "2026-04-20T23:35:30.418Z" }, + { url = "https://files.pythonhosted.org/packages/2e/42/c9853f8db3967fe08bcde11f53d53b85d351750cae726ce001cb68afa9c1/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", size = 5895175, upload-time = "2026-04-20T23:35:33.584Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fd/b82b5601a97630308bef079f545ffec481bbbc795c2ba5ec416a01d03f60/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e", size = 4110658, upload-time = "2026-04-20T23:35:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/62/8c/32ca69b0389ef25dd22937bf9e8fbe2ce27aea20b05ded48c4ce4cb42475/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", size = 3656251, upload-time = "2026-04-20T23:35:37.854Z" }, + { url = "https://files.pythonhosted.org/packages/c4/29/96992a2b59e3b9d730fcf9612d0a387305025dc867a9fc490a9e496e074e/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", size = 3301810, upload-time = "2026-04-20T23:35:39.927Z" }, + { url = "https://files.pythonhosted.org/packages/56/ad/44b06659949b243ae10112cd3b20a197f9bf3e81d5651379b9eb889bfaad/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", size = 3048977, upload-time = "2026-04-20T23:35:41.806Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f2/10a1bcebadb6aa55e280e1f58975c36a7b560ea525184c7aa4064c466633/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", size = 3351466, upload-time = "2026-04-20T23:35:43.993Z" }, + { url = "https://files.pythonhosted.org/packages/20/be/b732c8418ffa5bcfda002890f5dc4c869fc17db66ff11f53b17cfe44afc0/psycopg2_binary-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", size = 2848762, upload-time = "2026-04-20T23:35:46.421Z" }, +] + +[[package]] +name = "py-moneyed" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/d9/cb6f113d2b7cf930bb3963d2b84ee245c20f93f9afc2e41adece58d324ae/py-moneyed-3.0.tar.gz", hash = "sha256:4906f0f02cf2b91edba2e156f2d4e9a78f224059ab8c8fa2ff26230c75d894e8", size = 20385, upload-time = "2022-11-27T21:29:38.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/2b/2d87f12edc70f0d8c85148c51b57a4b8cf89e4d8ad291935a47ed1b7c52e/py_moneyed-3.0-py3-none-any.whl", hash = "sha256:9583a14f99c05b46196193d8185206e9b73c8439fc8a5eee9cfc7e733676d9bb", size = 11324, upload-time = "2022-11-27T21:29:37.201Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, +] + +[[package]] +name = "pycountry" +version = "26.2.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/1d/061b9e7a48b85cfd69f33c33d2ef784a531c359399ad764243399673c8f5/pycountry-26.2.16.tar.gz", hash = "sha256:5b6027d453fcd6060112b951dd010f01f168b51b4bf8a1f1fc8c95c8d94a0801", size = 7711342, upload-time = "2026-02-17T03:42:52.367Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/42/7703bd45b62fecd44cd7d3495423097e2f7d28bc2e99e7c1af68892ab157/pycountry-26.2.16-py3-none-any.whl", hash = "sha256:115c4baf7cceaa30f59a4694d79483c9167dbce7a9de4d3d571c5f3ea77c305a", size = 8044600, upload-time = "2026-02-17T03:42:49.777Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" }, + { url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" }, + { url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" }, + { url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" }, + { url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" }, + { url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" }, + { url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" }, + { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" }, + { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" }, + { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" }, + { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" }, + { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" }, + { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" }, + { url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" }, + { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" }, + { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" }, + { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" }, + { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" }, + { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" }, + { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" }, + { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" }, + { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" }, + { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" }, + { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" }, + { url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, +] + +[[package]] +name = "pydantic-extra-types" +version = "2.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/71/dba38ee2651f84f7842206adbd2233d8bbdb59fb85e9fa14232486a8c471/pydantic_extra_types-2.11.1.tar.gz", hash = "sha256:46792d2307383859e923d8fcefa82108b1a141f8a9c0198982b3832ab5ef1049", size = 172002, upload-time = "2026-03-16T08:08:03.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/c1/3226e6d7f5a4f736f38ac11a6fbb262d701889802595cdb0f53a885ac2e0/pydantic_extra_types-2.11.1-py3-none-any.whl", hash = "sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1", size = 79526, upload-time = "2026-03-16T08:08:02.533Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, +] + +[[package]] +name = "pyluach" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/11/42568c1568a75f8803c59f26d29af01a0890352b7a8e03d41ecda8bfb5dd/pyluach-2.3.0.tar.gz", hash = "sha256:ec6e30669d1df50c9ca160486da44a8195bb4c7a5d3d533990d0c5b03accd281", size = 26910, upload-time = "2025-09-09T20:24:39.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/c8/f96208ade3ca4c23b372497d0788bcf0f2e0ff4310e5ee693366bc33fdf0/pyluach-2.3.0-py3-none-any.whl", hash = "sha256:4497b731aef59508b079dbf5f00bc5bf4329ac45090a6cd37b5a83756f0e69ab", size = 25914, upload-time = "2025-09-09T20:24:37.831Z" }, +] + +[[package]] +name = "pymeeus" +version = "0.5.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/76/599896b37e60f43078afd8354b3802eb7ca257a7e7f6253cc21c4c672877/PyMeeus-0.5.12.tar.gz", hash = "sha256:548f7186bd8b96cbc069cf649a8e8e377dce49ac74486709849fe63a99cad684", size = 5752712, upload-time = "2022-12-11T11:37:48.504Z" } + +[[package]] +name = "pyod" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "matplotlib" }, + { name = "numba" }, + { name = "numpy" }, + { name = "scikit-learn" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/5d/1cf8e3b991989b8b573c9e0079a0385a6c1444a8702bef5241fd99e6a939/pyod-3.2.1.tar.gz", hash = "sha256:f7b729d601e6442e67cbf9d845f3200657ca5ae96359c07fa597afdbb87c1114", size = 304090, upload-time = "2026-04-16T20:36:25.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/83/1ae33ae6db890f20b32bf63a9333df271ec1edeca222e60d03faa512a7c4/pyod-3.2.1-py3-none-any.whl", hash = "sha256:258afe6d93a02935b221f19fc7bfa8e94b47a6e081cdd16d67f69bf377855bd5", size = 364719, upload-time = "2026-04-16T20:36:24.482Z" }, +] + +[[package]] +name = "pyomo" +version = "6.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ply" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/bf/29450fb25c87e7f37815190805e8f7af57ef259f6721bfed51ee547b5b44/Pyomo-6.8.2.tar.gz", hash = "sha256:40d8f7b216ad1602bb254f4296591608dd94fe2c961dc1e63ca6b84fb397bed6", size = 2877062, upload-time = "2024-11-18T22:55:21.872Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/c0/6c424e04e84bfd46626cf2f775f6e87ffeb9d55eb88b989eda7629b71e36/Pyomo-6.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd6dba83c33eb77320caaf6379b5e3149e670120b352d114a4c8508981f4eddf", size = 4055037, upload-time = "2024-11-19T00:00:58.211Z" }, + { url = "https://files.pythonhosted.org/packages/12/16/d7c048eb7ddabd195f0e4e058df01b17f970aa4cdcd5680883accd6fed30/Pyomo-6.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c91aa84c46deb1af8b802dfb35e4de0b8bb322e7bf0daf7c82b8430d512cbc98", size = 4045839, upload-time = "2024-11-19T00:01:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/23/01/8568d8729f2c85769b7ccd5c42c9d9d327995d7b1890da550c5936537364/Pyomo-6.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bad42b369b08c0181af54e57db74e23f9e58e18be23aa0881fac5be8e2149e32", size = 4108161, upload-time = "2024-11-19T00:01:13.383Z" }, + { url = "https://files.pythonhosted.org/packages/53/a6/db16eb1dd9e3b80230c213106f6e2f072ca5c014f36cfc5316a9c9e9415d/Pyomo-6.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35a6956b6128106366fd8f0ed86987289d962eaf084517e25419f1c6dec4f8a5", size = 4129490, upload-time = "2024-11-19T00:01:20.739Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/bd0986adc18c102033eaf63397ee3cae31f7d931c992a7cf75591688806a/Pyomo-6.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:462f304ce6ab44b9a1574d3c925199211568e59ea1aa05c5278aa67c6f56dd01", size = 4055222, upload-time = "2024-11-19T00:01:28.076Z" }, + { url = "https://files.pythonhosted.org/packages/df/cc/0af155f416834d9def8afb0715c8addb43115eaece8fdaf8ca07c9c7f0f7/Pyomo-6.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d3ad704366f6b41e96efdf0a5ce42bfe97aa17b4fed4fda3d265651d04cee2e", size = 4046024, upload-time = "2024-11-19T00:01:35.569Z" }, + { url = "https://files.pythonhosted.org/packages/f0/62/8b181a52f8d70740391bd8af6a111cd7053c44b05e94d0498a0f519d2c90/Pyomo-6.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54cabaa006d8743ebbefc7138fd036c3e0bbee793273e6847a363f248947a613", size = 4108054, upload-time = "2024-11-19T00:01:43.217Z" }, + { url = "https://files.pythonhosted.org/packages/7a/83/81fb8691660580c13437e0e453746e0d09af7aa34373ba8640ad7f9b6c8e/Pyomo-6.8.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd58d5d52e054e51fed1d5ff1937fb65fe77c8dad2d6b8b6a74c622c2eae0508", size = 4129544, upload-time = "2024-11-19T00:01:51.351Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d9/d4ae90087a1143d9e4889973b248f958440856f27469836a11ca66cc1500/Pyomo-6.8.2-py3-none-any.whl", hash = "sha256:7ea0790289da22964d1c2b37ca9eafda55373c84755db4c52c08fad5cde4ccd8", size = 3721249, upload-time = "2024-11-19T00:03:15.939Z" }, +] + +[[package]] +name = "pyopenssl" +version = "26.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/11/a62e1d33b373da2b2c2cd9eb508147871c80f12b1cacde3c5d314922afdd/pyopenssl-26.0.0.tar.gz", hash = "sha256:f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc", size = 185534, upload-time = "2026-03-15T14:28:26.353Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/7d/d4f7d908fa8415571771b30669251d57c3cf313b36a856e6d7548ae01619/pyopenssl-26.0.0-py3-none-any.whl", hash = "sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81", size = 57969, upload-time = "2026-03-15T14:28:24.864Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-keycloak" +version = "7.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "deprecation" }, + { name = "httpx" }, + { name = "jwcrypto" }, + { name = "requests" }, + { name = "requests-toolbelt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/e3/963aae33e9177b496def0ba47d2290391e9391d9f76a3f69cfde8c44c8b8/python_keycloak-7.1.1.tar.gz", hash = "sha256:38973e54694a656fe6f3f8fc2af0b89c78136863f928261f0d243445e9e486bc", size = 78907, upload-time = "2026-02-15T08:45:58.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/9f/569a8bbdb0859498d33d8b86273bc82849bd0b445ac01416ad34996ca3d8/python_keycloak-7.1.1-py3-none-any.whl", hash = "sha256:d8295bec6c4805ab7335b03bc92753c8c6258d5511b080cac061da20ae77f61c", size = 87607, upload-time = "2026-02-15T08:45:57.054Z" }, +] + +[[package]] +name = "pytz" +version = "2026.1.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "qrcode" +version = "8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/b2/7fc2931bfae0af02d5f53b174e9cf701adbb35f39d69c2af63d4a39f81a9/qrcode-8.2.tar.gz", hash = "sha256:35c3f2a4172b33136ab9f6b3ef1c00260dd2f66f858f24d88418a015f446506c", size = 43317, upload-time = "2025-05-01T15:44:24.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/b8/d2d6d731733f51684bbf76bf34dab3b70a9148e8f2cef2bb544fccec681a/qrcode-8.2-py3-none-any.whl", hash = "sha256:16e64e0716c14960108e85d853062c9e8bba5ca8252c0b4d0231b9df4060ff4f", size = 45986, upload-time = "2025-05-01T15:44:22.781Z" }, +] + +[[package]] +name = "readchar" +version = "4.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/49/a10341024c45bed95d13197ec9ef0f4e2fd10b5ca6e7f8d7684d18082398/readchar-4.2.2.tar.gz", hash = "sha256:e3b270fe16fc90c50ac79107700330a133dd4c63d22939f5b03b4f24564d5dd8", size = 9762, upload-time = "2026-04-06T19:45:54.226Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ca/36133653e00939922dd1416f4c56177361289172a30563fcb9552c9ccde4/readchar-4.2.2-py3-none-any.whl", hash = "sha256:92daf7e42c52b0787e6c75d01ecfb9a94f4ceff3764958b570c1dddedd47b200", size = 9401, upload-time = "2026-04-06T19:45:52.993Z" }, +] + +[[package]] +name = "redis" +version = "7.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/7f/3759b1d0d72b7c92f0d70ffd9dc962b7b7b5ee74e135f9d7d8ab06b8a318/redis-7.4.0.tar.gz", hash = "sha256:64a6ea7bf567ad43c964d2c30d82853f8df927c5c9017766c55a1d1ed95d18ad", size = 4943913, upload-time = "2026-03-24T09:14:37.53Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/3a/95deec7db1eb53979973ebd156f3369a72732208d1391cd2e5d127062a32/redis-7.4.0-py3-none-any.whl", hash = "sha256:a9c74a5c893a5ef8455a5adb793a31bb70feb821c86eccb62eebef5a19c429ec", size = 409772, upload-time = "2026-03-24T09:14:35.968Z" }, +] + +[[package]] +name = "redis-sentinel-url" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "redis" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/e3/68de4c8aacbd9952667d7d0f5af55b873553a09a6227ac5b7f7c622610c9/Redis-Sentinel-Url-1.0.1.tar.gz", hash = "sha256:ec1854ab9379a28789423c3cd4082739fb69c7b4b11bb50ae7858697c131b13e", size = 7599, upload-time = "2017-04-05T07:47:55.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/35/20f997f367c87ef1e6ebf418af7c2450cb5da860d066d7229226031534ad/Redis_Sentinel_Url-1.0.1-py2.py3-none-any.whl", hash = "sha256:c6991e2000c5c7a5e2b95eb2d62fd5b0a6b02a59554caf0f9f79d18e152d9663", size = 4685, upload-time = "2017-04-05T07:47:53.545Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[[package]] +name = "requests-file" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/f8/5dc70102e4d337063452c82e1f0d95e39abfe67aa222ed8a5ddeb9df8de8/requests_file-3.0.1.tar.gz", hash = "sha256:f14243d7796c588f3521bd423c5dea2ee4cc730e54a3cac9574d78aca1272576", size = 6967, upload-time = "2025-10-20T18:56:42.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/d5/de8f089119205a09da657ed4784c584ede8381a0ce6821212a6d4ca47054/requests_file-3.0.1-py2.py3-none-any.whl", hash = "sha256:d0f5eb94353986d998f80ac63c7f146a307728be051d4d1cd390dbdb59c10fa2", size = 4514, upload-time = "2025-10-20T18:56:41.184Z" }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, +] + +[[package]] +name = "rq" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "croniter" }, + { name = "redis" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/91/cdb1517255b28fdaa44d0377caa810524e6a38d24373e5ef3911b04e4228/rq-2.8.0.tar.gz", hash = "sha256:dd75b5a19016efd235143058e82fdc5658a0c1e9a76664cc7d02f721c7308a4a", size = 743395, upload-time = "2026-04-17T00:21:13.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/07/9a8c6ac2440f8e532260adaa3fe4a8f7edfcac4f038f3428e71cb32e13e2/rq-2.8.0-py3-none-any.whl", hash = "sha256:49d87c8d0068b890e83052050ffd18be328339ae00c9c6d5dbf2702eb06107d2", size = 119484, upload-time = "2026-04-17T00:21:11.513Z" }, +] + +[[package]] +name = "rq-dashboard" +version = "0.8.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arrow" }, + { name = "flask" }, + { name = "redis" }, + { name = "redis-sentinel-url" }, + { name = "rq" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/74/01ad052d8aec0e0d31a9575bd4e19a0aa62235c0e8a259cf7fab12b1fda5/rq_dashboard-0.8.6.tar.gz", hash = "sha256:36a3182a6ea854642d444f8691bb4e692afb36394429cd8c2c4963fbc8cbb572", size = 210231, upload-time = "2025-12-02T08:36:01.354Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/5e/8b92e99341fd81f7349a5614756c3e3bfc1ec8cae6dbc4b45649ec07fac3/rq_dashboard-0.8.6-py2.py3-none-any.whl", hash = "sha256:dc7b3517538bfe95e4ac9fe3a3017af939f940da88a79edd5cf40f26dca4ce67", size = 213580, upload-time = "2025-12-02T08:35:59.942Z" }, +] + +[[package]] +name = "rq-win" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rq" }, + { name = "times" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/fe/92187f1ebb14760299e401e29ed6c1f4f3d415ed130fbc1575f148689242/rq_win-0.4.2.tar.gz", hash = "sha256:f7c4a573a225ffc48044050d6b2b188e8423ffaec318d80af94462b6751f9478", size = 4550, upload-time = "2025-06-09T07:47:34.642Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/8d/57d2020ca2deadb4b9aa2cb2d274695d9ee110cee1fbf69beacc34a442f6/rq_win-0.4.2-py3-none-any.whl", hash = "sha256:f7c5cf1a7f7c846988e59099901c973eacd50badfe50e2c00b9180b567a68482", size = 4741, upload-time = "2025-06-09T07:47:33.235Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, +] + +[[package]] +name = "scikit-base" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/47/7954094adee334012d7182c2f78bb5f134f6ec93e28a0fdc6856de5ed4f6/scikit_base-0.13.2.tar.gz", hash = "sha256:c78245064754781b17062ab183a03bc12b36163b9e5f2cfcb6f29f749be4b266", size = 135452, upload-time = "2026-04-13T21:14:50.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/2e/1488716735c31bf82c6898759d12771b3e0ff5daeb0f01b73b82a4462ad9/scikit_base-0.13.2-py3-none-any.whl", hash = "sha256:0a67063c0323b9521ede91cfcc87ce097126de5f9d5367abed6e76ff97a0eda5", size = 160792, upload-time = "2026-04-13T21:14:48.243Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" }, + { url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/93/a3038cb0293037fd335f77f31fe053b89c72f17b1c8908c576c29d953e84/scikit_learn-1.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7", size = 9212382, upload-time = "2025-09-09T08:20:54.731Z" }, + { url = "https://files.pythonhosted.org/packages/40/dd/9a88879b0c1104259136146e4742026b52df8540c39fec21a6383f8292c7/scikit_learn-1.7.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe", size = 8592042, upload-time = "2025-09-09T08:20:57.313Z" }, + { url = "https://files.pythonhosted.org/packages/46/af/c5e286471b7d10871b811b72ae794ac5fe2989c0a2df07f0ec723030f5f5/scikit_learn-1.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f", size = 9434180, upload-time = "2025-09-09T08:20:59.671Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fd/df59faa53312d585023b2da27e866524ffb8faf87a68516c23896c718320/scikit_learn-1.7.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0", size = 9283660, upload-time = "2025-09-09T08:21:01.71Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c7/03000262759d7b6f38c836ff9d512f438a70d8a8ddae68ee80de72dcfb63/scikit_learn-1.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c", size = 8702057, upload-time = "2025-09-09T08:21:04.234Z" }, + { url = "https://files.pythonhosted.org/packages/55/87/ef5eb1f267084532c8e4aef98a28b6ffe7425acbfd64b5e2f2e066bc29b3/scikit_learn-1.7.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8", size = 9558731, upload-time = "2025-09-09T08:21:06.381Z" }, + { url = "https://files.pythonhosted.org/packages/93/f8/6c1e3fc14b10118068d7938878a9f3f4e6d7b74a8ddb1e5bed65159ccda8/scikit_learn-1.7.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a", size = 9038852, upload-time = "2025-09-09T08:21:08.628Z" }, + { url = "https://files.pythonhosted.org/packages/83/87/066cafc896ee540c34becf95d30375fe5cbe93c3b75a0ee9aa852cd60021/scikit_learn-1.7.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c", size = 9527094, upload-time = "2025-09-09T08:21:11.486Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2b/4903e1ccafa1f6453b1ab78413938c8800633988c838aa0be386cbb33072/scikit_learn-1.7.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c", size = 9367436, upload-time = "2025-09-09T08:21:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/b5/aa/8444be3cfb10451617ff9d177b3c190288f4563e6c50ff02728be67ad094/scikit_learn-1.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973", size = 9275749, upload-time = "2025-09-09T08:21:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/dee5acf66837852e8e68df6d8d3a6cb22d3df997b733b032f513d95205b7/scikit_learn-1.7.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33", size = 9208906, upload-time = "2025-09-09T08:21:18.557Z" }, + { url = "https://files.pythonhosted.org/packages/3c/30/9029e54e17b87cb7d50d51a5926429c683d5b4c1732f0507a6c3bed9bf65/scikit_learn-1.7.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615", size = 8627836, upload-time = "2025-09-09T08:21:20.695Z" }, + { url = "https://files.pythonhosted.org/packages/60/18/4a52c635c71b536879f4b971c2cedf32c35ee78f48367885ed8025d1f7ee/scikit_learn-1.7.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106", size = 9426236, upload-time = "2025-09-09T08:21:22.645Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/290362f6ab582128c53445458a5befd471ed1ea37953d5bcf80604619250/scikit_learn-1.7.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61", size = 9312593, upload-time = "2025-09-09T08:21:24.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/87/24f541b6d62b1794939ae6422f8023703bbf6900378b2b34e0b4384dfefd/scikit_learn-1.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8", size = 8820007, upload-time = "2025-09-09T08:21:26.713Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.58.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/b3/fb8291170d0e844173164709fc0fa0c221ed75a5da740c8746f2a83b4eb1/sentry_sdk-2.58.0.tar.gz", hash = "sha256:c1144d947352d54e5b7daa63596d9f848adf684989c06c4f5a659f0c85a18f6f", size = 438764, upload-time = "2026-04-13T17:23:26.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/eb/d875669993b762556ae8b2efd86219943b4c0864d22204d622a9aee3052b/sentry_sdk-2.58.0-py2.py3-none-any.whl", hash = "sha256:688d1c704ddecf382ea3326f21a67453d4caa95592d722b7c780a36a9d23109e", size = 460919, upload-time = "2026-04-13T17:23:24.675Z" }, +] + +[package.optional-dependencies] +flask = [ + { name = "blinker" }, + { name = "flask" }, + { name = "markupsafe" }, +] + +[[package]] +name = "shap" +version = "0.49.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "numba" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "slicer" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/c6/9823a7f483aa9f3179fc359c10d22da9e418b1a7a3fc99a42b705d05e82a/shap-0.49.1.tar.gz", hash = "sha256:1114ecd804fff29f50d522ce6031082fcf42fe4a32fb1b5da233b2415d784c8c", size = 4084725, upload-time = "2025-10-14T10:04:49.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/7a/ccecf7a9158baa10bdc5146907c72dd5f85c762cb5f16cdc74d15cebb8a1/shap-0.49.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c652dc77f1fffe73f5a3def3356c5090e2e6401c261e4fe5329d83cb6251e772", size = 559663, upload-time = "2025-10-14T10:04:25.412Z" }, + { url = "https://files.pythonhosted.org/packages/ee/c6/c43382d6c891fcf067d0a9f6d954351e3c7d330f4328c5816769b796aa27/shap-0.49.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c23f1493205e648634680c8974e82e7f4b2e96ae3a7eca2251680172bd197ae9", size = 556265, upload-time = "2025-10-14T10:04:27.098Z" }, + { url = "https://files.pythonhosted.org/packages/c0/71/f7db7a5a2cedaa3ac52f58f453172d613be041bedd9509ce5b5cba2096a6/shap-0.49.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41147740c42821023e1b60185ce8be989656ccac266cc9490d7a8e3ad53c556a", size = 1022419, upload-time = "2025-10-14T10:04:28.793Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a4/96ca9a69dd669ff835ddef875c5dd8e07599103769417d3e9051fd97d470/shap-0.49.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef9952929d4a7e6763d2716938067bdad762217e3afb46cabfc15a62c012b364", size = 1027074, upload-time = "2025-10-14T10:04:30.2Z" }, + { url = "https://files.pythonhosted.org/packages/fc/9a/89ed1ac8beffe8ff8e09c12cb351bc3c79ddaadcc47ca6ee434d76e464d7/shap-0.49.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e823417eb0a01947cd9bd763bef2e534c5aef7a7c2952b1badfa969c7d59d3b3", size = 2088172, upload-time = "2025-10-14T10:04:31.725Z" }, + { url = "https://files.pythonhosted.org/packages/4a/28/11422c1c3aa022a06e76cbfa3267e1750cedc00c1e02ef1ccae9c88cd6f4/shap-0.49.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb28043decfec3f35f795421eb5a81545f629b7f60bbf7449cd2843a7f1c8cc6", size = 548036, upload-time = "2025-10-14T10:04:33.087Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5c/030bbfa19605ca4ad66a753d55e76aee5093be6748a6d33eda89e5613995/shap-0.49.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:333cd8e8c427badda92d5ada9e7aad1e3e1e8e7e0398da51a18b7ffb03514e45", size = 558604, upload-time = "2025-10-14T10:04:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7f/7e7b78e9fac6f891096fb6a59a6d4db23243b0af2369ae54e161f513c485/shap-0.49.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4faf61560f73a66f4f26bc027c91f8939201979c4db24949dca305ba0a2ad36", size = 555311, upload-time = "2025-10-14T10:04:35.582Z" }, + { url = "https://files.pythonhosted.org/packages/f2/be/25283a0f8c30deaf897b89a0dbfd490d330f6fc68caa6f19db6e130832e9/shap-0.49.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b440da658d9aee7711bf642c9b4826d81f588fb478cd9e90c068646e90f56669", size = 1016897, upload-time = "2025-10-14T10:04:36.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/91/a63e563f3dc8e134db12dd155a1a6ed5e0649f79fc8ac651aac1088e8652/shap-0.49.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8dfa5654eccf4d13dcb262a10314a4e0eb1060db842b2ef31e9fb0038168bc1", size = 1022476, upload-time = "2025-10-14T10:04:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/15/a2/89303c1f7eb206658bf9ec974dc6e69b0a6bd309cf5de0cfa8f92f5a8eb3/shap-0.49.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ed3080030a6000d3737841c5770ed555b8a922b794fa0ba5aae1e45655eda1fa", size = 2087940, upload-time = "2025-10-14T10:04:39.497Z" }, + { url = "https://files.pythonhosted.org/packages/84/bd/0b9b3e19b9b8cda51463f8a749dc354eb9c87f42eddcbfdf742dceb3746b/shap-0.49.1-cp313-cp313-win_amd64.whl", hash = "sha256:6af779344c23b12a47063aab7fc135fefbdb5849233c1813f11dd8cf2fc73bea", size = 547806, upload-time = "2025-10-14T10:04:40.712Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sktime" +version = "0.40.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "scikit-base" }, + { name = "scikit-learn" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/7a/22b99a00a7cabaa34c350f78f33ae89711cfc08deef06043daee3835dbb3/sktime-0.40.1.tar.gz", hash = "sha256:bbf3c2971c7ad388fbebc1d1fb573e20f730e6486c00935b336d8edf13bca04d", size = 35197759, upload-time = "2025-11-25T00:03:38.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/0d/3b2e5b51593009f72fb7fb6956e6c652da1450c8c5484c40a276113c2114/sktime-0.40.1-py3-none-any.whl", hash = "sha256:130bb0c39ca4377e491c4dfa64c8ac67c3de2660dd406dd7690db6a8ac349c4e", size = 36265753, upload-time = "2025-11-25T00:03:34.522Z" }, +] + +[[package]] +name = "slicer" +version = "0.0.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/f9/b4bce2825b39b57760b361e6131a3dacee3d8951c58cb97ad120abb90317/slicer-0.0.8.tar.gz", hash = "sha256:2e7553af73f0c0c2d355f4afcc3ecf97c6f2156fcf4593955c3f56cf6c4d6eb7", size = 14894, upload-time = "2024-03-09T23:35:26.826Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/81/9ef641ff4e12cbcca30e54e72fb0951a2ba195d0cda0ba4100e532d929db/slicer-0.0.8-py3-none-any.whl", hash = "sha256:6c206258543aecd010d497dc2eca9d2805860a0b3758673903456b7df7934dc3", size = 15251, upload-time = "2024-03-09T07:03:07.708Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.49" +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/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b", size = 2157681, upload-time = "2026-04-03T16:53:07.132Z" }, + { url = "https://files.pythonhosted.org/packages/50/84/b2a56e2105bd11ebf9f0b93abddd748e1a78d592819099359aa98134a8bf/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982", size = 3338976, upload-time = "2026-04-03T17:07:40Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672", size = 3351937, upload-time = "2026-04-03T17:12:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2f/6fd118563572a7fe475925742eb6b3443b2250e346a0cc27d8d408e73773/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e", size = 3281646, upload-time = "2026-04-03T17:07:41.949Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d7/410f4a007c65275b9cf82354adb4bb8ba587b176d0a6ee99caa16fe638f8/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750", size = 3316695, upload-time = "2026-04-03T17:12:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/d9/95/81f594aa60ded13273a844539041ccf1e66c5a7bed0a8e27810a3b52d522/sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0", size = 2117483, upload-time = "2026-04-03T17:05:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4", size = 2144494, upload-time = "2026-04-03T17:05:42.282Z" }, + { url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547, upload-time = "2026-04-03T16:53:08.64Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782, upload-time = "2026-04-03T17:07:43.508Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156, upload-time = "2026-04-03T17:12:27.697Z" }, + { url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832, upload-time = "2026-04-03T17:07:45.38Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000, upload-time = "2026-04-03T17:12:29.657Z" }, + { url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641, upload-time = "2026-04-03T17:05:43.989Z" }, + { url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498, upload-time = "2026-04-03T17:05:45.7Z" }, + { url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807, upload-time = "2026-04-03T16:58:31.666Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481, upload-time = "2026-04-03T17:06:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565, upload-time = "2026-04-03T16:58:33.414Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769, upload-time = "2026-04-03T17:06:02.787Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319, upload-time = "2026-04-03T17:02:04.328Z" }, + { url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104, upload-time = "2026-04-03T17:02:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e", size = 2156356, upload-time = "2026-04-03T16:53:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a7/5f476227576cb8644650eff68cc35fa837d3802b997465c96b8340ced1e2/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a", size = 3276486, upload-time = "2026-04-03T17:07:46.9Z" }, + { url = "https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066", size = 3281479, upload-time = "2026-04-03T17:12:32.226Z" }, + { url = "https://files.pythonhosted.org/packages/91/68/bb406fa4257099c67bd75f3f2261b129c63204b9155de0d450b37f004698/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187", size = 3226269, upload-time = "2026-04-03T17:07:48.678Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/acb56c00cca9f251f437cb49e718e14f7687505749ea9255d7bd8158a6df/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401", size = 3248260, upload-time = "2026-04-03T17:12:34.381Z" }, + { url = "https://files.pythonhosted.org/packages/56/19/6a20ea25606d1efd7bd1862149bb2a22d1451c3f851d23d887969201633f/sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5", size = 2118463, upload-time = "2026-04-03T17:05:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5", size = 2144204, upload-time = "2026-04-03T17:05:48.694Z" }, + { url = "https://files.pythonhosted.org/packages/1f/33/95e7216df810c706e0cd3655a778604bbd319ed4f43333127d465a46862d/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977", size = 3565474, upload-time = "2026-04-03T16:58:35.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a4/ed7b18d8ccf7f954a83af6bb73866f5bc6f5636f44c7731fbb741f72cc4f/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01", size = 3530567, upload-time = "2026-04-03T17:06:04.587Z" }, + { url = "https://files.pythonhosted.org/packages/73/a3/20faa869c7e21a827c4a2a42b41353a54b0f9f5e96df5087629c306df71e/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61", size = 3474282, upload-time = "2026-04-03T16:58:37.131Z" }, + { url = "https://files.pythonhosted.org/packages/b7/50/276b9a007aa0764304ad467eceb70b04822dc32092492ee5f322d559a4dc/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a", size = 3480406, upload-time = "2026-04-03T17:06:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c3/c80fcdb41905a2df650c2a3e0337198b6848876e63d66fe9188ef9003d24/sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158", size = 2149151, upload-time = "2026-04-03T17:02:07.281Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9f1a62feab6ed368aff068524ff414f26a6daebc7361861035ae00b05530/sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7", size = 2184178, upload-time = "2026-04-03T17:02:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" }, +] + +[[package]] +name = "statsmodels" +version = "0.14.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "patsy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/81/e8d74b34f85285f7335d30c5e3c2d7c0346997af9f3debf9a0a9a63de184/statsmodels-0.14.6.tar.gz", hash = "sha256:4d17873d3e607d398b85126cd4ed7aad89e4e9d89fc744cdab1af3189a996c2a", size = 20689085, upload-time = "2025-12-05T23:08:39.522Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/ce/308e5e5da57515dd7cab3ec37ea2d5b8ff50bef1fcc8e6d31456f9fae08e/statsmodels-0.14.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe76140ae7adc5ff0e60a3f0d56f4fffef484efa803c3efebf2fcd734d72ecb5", size = 10091932, upload-time = "2025-12-05T19:28:55.446Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/affbabf3c27fb501ec7b5808230c619d4d1a4525c07301074eb4bda92fa9/statsmodels-0.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26d4f0ed3b31f3c86f83a92f5c1f5cbe63fc992cd8915daf28ca49be14463a1c", size = 9997345, upload-time = "2025-12-05T19:29:10.278Z" }, + { url = "https://files.pythonhosted.org/packages/48/f5/3a73b51e6450c31652c53a8e12e24eac64e3824be816c0c2316e7dbdcb7d/statsmodels-0.14.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8c00a42863e4f4733ac9d078bbfad816249c01451740e6f5053ecc7db6d6368", size = 10058649, upload-time = "2025-12-05T23:10:12.775Z" }, + { url = "https://files.pythonhosted.org/packages/81/68/dddd76117df2ef14c943c6bbb6618be5c9401280046f4ddfc9fb4596a1b8/statsmodels-0.14.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b58cf7474aa9e7e3b0771a66537148b2df9b5884fbf156096c0e6c1ff0469d", size = 10339446, upload-time = "2025-12-05T23:10:28.503Z" }, + { url = "https://files.pythonhosted.org/packages/56/4a/dce451c74c4050535fac1ec0c14b80706d8fc134c9da22db3c8a0ec62c33/statsmodels-0.14.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81e7dcc5e9587f2567e52deaff5220b175bf2f648951549eae5fc9383b62bc37", size = 10368705, upload-time = "2025-12-05T23:10:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/60/15/3daba2df40be8b8a9a027d7f54c8dedf24f0d81b96e54b52293f5f7e3418/statsmodels-0.14.6-cp312-cp312-win_amd64.whl", hash = "sha256:b5eb07acd115aa6208b4058211138393a7e6c2cf12b6f213ede10f658f6a714f", size = 9543991, upload-time = "2025-12-05T23:10:58.536Z" }, + { url = "https://files.pythonhosted.org/packages/81/59/a5aad5b0cc266f5be013db8cde563ac5d2a025e7efc0c328d83b50c72992/statsmodels-0.14.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47ee7af083623d2091954fa71c7549b8443168f41b7c5dce66510274c50fd73e", size = 10072009, upload-time = "2025-12-05T23:11:14.021Z" }, + { url = "https://files.pythonhosted.org/packages/53/dd/d8cfa7922fc6dc3c56fa6c59b348ea7de829a94cd73208c6f8202dd33f17/statsmodels-0.14.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa60d82e29fcd0a736e86feb63a11d2380322d77a9369a54be8b0965a3985f71", size = 9980018, upload-time = "2025-12-05T23:11:30.907Z" }, + { url = "https://files.pythonhosted.org/packages/ee/77/0ec96803eba444efd75dba32f2ef88765ae3e8f567d276805391ec2c98c6/statsmodels-0.14.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89ee7d595f5939cc20bf946faedcb5137d975f03ae080f300ebb4398f16a5bd4", size = 10060269, upload-time = "2025-12-05T23:11:46.338Z" }, + { url = "https://files.pythonhosted.org/packages/10/b9/fd41f1f6af13a1a1212a06bb377b17762feaa6d656947bf666f76300fc05/statsmodels-0.14.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:730f3297b26749b216a06e4327fe0be59b8d05f7d594fb6caff4287b69654589", size = 10324155, upload-time = "2025-12-05T23:12:01.805Z" }, + { url = "https://files.pythonhosted.org/packages/ee/0f/a6900e220abd2c69cd0a07e3ad26c71984be6061415a60e0f17b152ecf08/statsmodels-0.14.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f1c08befa85e93acc992b72a390ddb7bd876190f1360e61d10cf43833463bc9c", size = 10349765, upload-time = "2025-12-05T23:12:18.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/08/b79f0c614f38e566eebbdcff90c0bcacf3c6ba7a5bbb12183c09c29ca400/statsmodels-0.14.6-cp313-cp313-win_amd64.whl", hash = "sha256:8021271a79f35b842c02a1794465a651a9d06ec2080f76ebc3b7adce77d08233", size = 9540043, upload-time = "2025-12-05T23:12:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/71/de/09540e870318e0c7b58316561d417be45eff731263b4234fdd2eee3511a8/statsmodels-0.14.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:00781869991f8f02ad3610da6627fd26ebe262210287beb59761982a8fa88cae", size = 10069403, upload-time = "2025-12-05T23:12:48.424Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f0/63c1bfda75dc53cee858006e1f46bd6d6f883853bea1b97949d0087766ca/statsmodels-0.14.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:73f305fbf31607b35ce919fae636ab8b80d175328ed38fdc6f354e813b86ee37", size = 9989253, upload-time = "2025-12-05T23:13:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/c1/98/b0dfb4f542b2033a3341aa5f1bdd97024230a4ad3670c5b0839d54e3dcab/statsmodels-0.14.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e443e7077a6e2d3faeea72f5a92c9f12c63722686eb80bb40a0f04e4a7e267ad", size = 10090802, upload-time = "2025-12-05T23:13:20.653Z" }, + { url = "https://files.pythonhosted.org/packages/34/0e/2408735aca9e764643196212f9069912100151414dd617d39ffc72d77eee/statsmodels-0.14.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3414e40c073d725007a6603a18247ab7af3467e1af4a5e5a24e4c27bc26673b4", size = 10337587, upload-time = "2025-12-05T23:13:37.597Z" }, + { url = "https://files.pythonhosted.org/packages/0f/36/4d44f7035ab3c0b2b6a4c4ebb98dedf36246ccbc1b3e2f51ebcd7ac83abb/statsmodels-0.14.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a518d3f9889ef920116f9fa56d0338069e110f823926356946dae83bc9e33e19", size = 10363350, upload-time = "2025-12-05T23:13:53.08Z" }, + { url = "https://files.pythonhosted.org/packages/26/33/f1652d0c59fa51de18492ee2345b65372550501ad061daa38f950be390b6/statsmodels-0.14.6-cp314-cp314-win_amd64.whl", hash = "sha256:151b73e29f01fe619dbce7f66d61a356e9d1fe5e906529b78807df9189c37721", size = 9588010, upload-time = "2025-12-05T23:14:07.28Z" }, +] + +[[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 = "testcontainers" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docker" }, + { name = "python-dotenv" }, + { name = "typing-extensions" }, + { name = "urllib3" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/ac/a597c3a0e02b26cbed6dd07df68be1e57684766fd1c381dee9b170a99690/testcontainers-4.14.2.tar.gz", hash = "sha256:1340ccf16fe3acd9389a6c9e1d9ab21d9fe99a8afdf8165f89c3e69c1967d239", size = 166841, upload-time = "2026-03-18T05:19:16.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/2d/26b8b30067d94339afee62c3edc9b803a6eb9332f521ba77d8aaab5de873/testcontainers-4.14.2-py3-none-any.whl", hash = "sha256:0d0522c3cd8f8d9627cda41f7a6b51b639fa57bdc492923c045117933c668d68", size = 125712, upload-time = "2026-03-18T05:19:15.29Z" }, +] + +[package.optional-dependencies] +keycloak = [ + { name = "python-keycloak", marker = "python_full_version < '4'" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "timely-beliefs" +version = "3.5.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "isodate" }, + { name = "numpy" }, + { name = "openturns" }, + { name = "pandas" }, + { name = "properscoring" }, + { name = "psycopg2-binary" }, + { name = "pytz" }, + { name = "scipy" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/bb/9369de8f0af17b9063c70073cb63b4d6d32d2ac16105ad1967cce1e393e2/timely_beliefs-3.5.5.tar.gz", hash = "sha256:c165db5e92fb3557296b6dbfb9b526de27729f6bb5b23b1b4d65ea1b407216d5", size = 793855, upload-time = "2026-05-04T15:10:38.682Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/76/2032d023b3540f4cbeb3fe0fa45f1c50b036ab30beaeb0bf8257042b0d7c/timely_beliefs-3.5.5-py2.py3-none-any.whl", hash = "sha256:bb09c96ddc10b7ccd4520395acd29df7737ba79ed65ce1c8335cd03d98ba0c31", size = 807541, upload-time = "2026-05-04T15:10:36.885Z" }, +] + +[package.optional-dependencies] +forecast = [ + { name = "numpy" }, + { name = "sktime" }, +] + +[[package]] +name = "times" +version = "0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arrow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/be/ec94886bf2540dca5c120950b290f6b4aa42c35476d4a82aefb19de0ebe9/times-0.7.tar.gz", hash = "sha256:ed9da7bd0384ff4e1b3fc84114c27c2e3987072dbaf24425340bfedd405bc4b5", size = 4228, upload-time = "2014-08-24T06:13:25.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/2f/811d8183abae998effbbb1a191af2469a6c18bfcb4d3edfd4fe9c096c5f8/times-0.7-py2.py3-none-any.whl", hash = "sha256:71a47e488fb727fecbd2bc6b4e3559fd4425eb6e20c68b1418069325a4c58a37", size = 3826, upload-time = "2014-08-24T06:13:21.84Z" }, +] + +[[package]] +name = "timetomodel" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "sqlalchemy" }, + { name = "statsmodels" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/51/948aa9b4e8498924181eb3d17533eff0b43483d46ca9201b6c96d24b410c/timetomodel-0.7.3.tar.gz", hash = "sha256:dd57e20f1fea58240d8a93f38d0d9f598fc0600e88eeac5dceecbf34a750e08c", size = 141996, upload-time = "2023-06-07T13:37:05.398Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/2d/379105ee6650157b650c5a0da42ccab698fae50687d3ee481a2532bb1ba6/timetomodel-0.7.3-py2.py3-none-any.whl", hash = "sha256:031f0d2cd8d6320c9a8dfd35cf3d976bdc4f0d74f9322690d6ad74d05445ff05", size = 29820, upload-time = "2023-06-07T13:37:03.356Z" }, +] + +[[package]] +name = "tldextract" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "idna" }, + { name = "requests" }, + { name = "requests-file" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/7b/644fbbb49564a6cb124a8582013315a41148dba2f72209bba14a84242bf0/tldextract-5.3.1.tar.gz", hash = "sha256:a72756ca170b2510315076383ea2993478f7da6f897eef1f4a5400735d5057fb", size = 126105, upload-time = "2025-12-28T23:58:05.532Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/42/0e49d6d0aac449ca71952ec5bae764af009754fcb2e76a5cc097543747b3/tldextract-5.3.1-py3-none-any.whl", hash = "sha256:6bfe36d518de569c572062b788e16a659ccaceffc486d243af0484e8ecf432d9", size = 105886, upload-time = "2025-12-28T23:58:04.071Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "typeguard" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/e8/66e25efcc18542d58706ce4e50415710593721aae26e794ab1dec34fb66f/typeguard-4.5.1.tar.gz", hash = "sha256:f6f8ecbbc819c9bc749983cc67c02391e16a9b43b8b27f15dc70ed7c4a007274", size = 80121, upload-time = "2026-02-19T16:09:03.392Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl", hash = "sha256:44d2bf329d49a244110a090b55f5f91aa82d9a9834ebfd30bcc73651e4a8cc40", size = 36745, upload-time = "2026-02-19T16:09:01.6Z" }, +] + +[[package]] +name = "types-cffi" +version = "2.0.0.20260408" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/67/eb4ef3408fdc0b4e5af38b30c0e6ad4663b41bdae9fb85a9f09a8db61a99/types_cffi-2.0.0.20260408.tar.gz", hash = "sha256:aa8b9c456ab715c079fc655929811f21f331bfb940f4a821987c581bf4e36230", size = 17541, upload-time = "2026-04-08T04:36:03.918Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/a3/7fbd93ededcc7c77e9e5948b9794161733ebdbf618a27965b1bea0e728a4/types_cffi-2.0.0.20260408-py3-none-any.whl", hash = "sha256:68bd296742b4ff7c0afe3547f50bd0acc55416ecf322ffefd2b7344ef6388a42", size = 20101, upload-time = "2026-04-08T04:36:02.995Z" }, +] + +[[package]] +name = "types-click" +version = "7.1.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/ff/0e6a56108d45c80c61cdd4743312d0304d8192482aea4cce96c554aaa90d/types-click-7.1.8.tar.gz", hash = "sha256:b6604968be6401dc516311ca50708a0a28baa7a0cb840efd7412f0dbbff4e092", size = 10015, upload-time = "2021-11-23T12:28:01.701Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/ad/607454a5f991c5b3e14693a7113926758f889138371058a5f72f567fa131/types_click-7.1.8-py3-none-any.whl", hash = "sha256:8cb030a669e2e927461be9827375f83c16b8178c365852c060a34e24871e7e81", size = 12929, upload-time = "2021-11-23T12:27:59.493Z" }, +] + +[[package]] +name = "types-docker" +version = "7.1.0.20260518" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-paramiko" }, + { name = "types-requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/71/ee4bd2b713f0b3b0cecc48fd13952b244f187c3b68cfa0408360f102fde8/types_docker-7.1.0.20260518.tar.gz", hash = "sha256:d194c4b82a4110fc58c84af05a5d9de6d3e54f94357ff745d01b5dca12c8eaaa", size = 33868, upload-time = "2026-05-18T06:08:19.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/1d/1d174b6f94afe0de50189de0a9dfd4531d2f8170e6dd9ef68ab616aa770b/types_docker-7.1.0.20260518-py3-none-any.whl", hash = "sha256:59893937816bfe40eb2915c84b31c3131107537500287c47d4a10dd7a9d59deb", size = 48183, upload-time = "2026-05-18T06:08:18.89Z" }, +] + +[[package]] +name = "types-flask" +version = "1.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-click" }, + { name = "types-jinja2" }, + { name = "types-werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/65/728a104973133a45fba50f3d1e1ee832287666ac74cfd47004cea8402ea3/types-Flask-1.1.6.tar.gz", hash = "sha256:aac777b3abfff9436e6b01f6d08171cf23ea6e5be71cbf773aaabb1c5763e9cf", size = 9829, upload-time = "2021-11-26T06:21:31.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/6c/a98a0c29c39d8a6283ac704f3d36f0d570d8dee931e9d46d6cc60d436bec/types_Flask-1.1.6-py3-none-any.whl", hash = "sha256:6ab8a9a5e258b76539d652f6341408867298550b19b81f0e41e916825fc39087", size = 13733, upload-time = "2021-11-26T06:21:30.365Z" }, +] + +[[package]] +name = "types-jinja2" +version = "2.11.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/c4/b82309bfed8195de7997672deac301bd6f5bd5cbb6a3e392b7fe780d7852/types-Jinja2-2.11.9.tar.gz", hash = "sha256:dbdc74a40aba7aed520b7e4d89e8f0fe4286518494208b35123bcf084d4b8c81", size = 13302, upload-time = "2021-11-26T06:21:17.496Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b0/e79d84748f1d34304f13191424348a719c3febaa3493835370fe9528e1e6/types_Jinja2-2.11.9-py3-none-any.whl", hash = "sha256:60a1e21e8296979db32f9374d8a239af4cb541ff66447bb915d8ad398f9c63b2", size = 18190, upload-time = "2021-11-26T06:21:16.18Z" }, +] + +[[package]] +name = "types-markupsafe" +version = "1.1.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/31/b5f059142d058aec41e913d8e0eff0a967e7bc46f9a2ba2f31bc11cff059/types-MarkupSafe-1.1.10.tar.gz", hash = "sha256:85b3a872683d02aea3a5ac2a8ef590193c344092032f58457287fbf8e06711b1", size = 2986, upload-time = "2021-11-27T03:18:07.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/d6/b8effb1c48539260a5eb4196afc55efac4ea1684a4991977555eb266b2ef/types_MarkupSafe-1.1.10-py3-none-any.whl", hash = "sha256:ca2bee0f4faafc45250602567ef38d533e877d2ddca13003b319c551ff5b3cc5", size = 3998, upload-time = "2021-11-27T03:18:06.398Z" }, +] + +[[package]] +name = "types-paramiko" +version = "4.0.0.20260518" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/b6/4cdb11fb6006be6309844dc5f88b6efef3f5ea5352ade42a1e9308570e28/types_paramiko-4.0.0.20260518.tar.gz", hash = "sha256:286f6830945cba63797eedf375ed87138d93198121253afe66c5d6dbcf91318d", size = 29193, upload-time = "2026-05-18T06:06:36.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/a2/1a54b77758c9c175526bd0448de353f0563e71ba1ddc8bd4ac0c835deafd/types_paramiko-4.0.0.20260518-py3-none-any.whl", hash = "sha256:0ffaf1a6eb796833a49653cba4c7be13af51c8269d75234972d6239763dda270", size = 38791, upload-time = "2026-05-18T06:06:35.771Z" }, +] + +[[package]] +name = "types-pyopenssl" +version = "24.1.0.20240722" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "types-cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/29/47a346550fd2020dac9a7a6d033ea03fccb92fa47c726056618cc889745e/types-pyOpenSSL-24.1.0.20240722.tar.gz", hash = "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39", size = 8458, upload-time = "2024-07-22T02:32:22.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/05/c868a850b6fbb79c26f5f299b768ee0adc1f9816d3461dcf4287916f655b/types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl", hash = "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54", size = 7499, upload-time = "2024-07-22T02:32:21.232Z" }, +] + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/f3/2427775f80cd5e19a0a71ba8e5ab7645a01a852f43a5fd0ffc24f66338e0/types_python_dateutil-2.9.0.20260408.tar.gz", hash = "sha256:8b056ec01568674235f64ecbcef928972a5fac412f5aab09c516dfa2acfbb582", size = 16981, upload-time = "2026-04-08T04:28:10.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/c6/eeba37bfee282a6a97f889faef9352d6172c6a5088eb9a4daf570d9d748d/types_python_dateutil-2.9.0.20260408-py3-none-any.whl", hash = "sha256:473139d514a71c9d1fbd8bb328974bedcb1cc3dba57aad04ffa4157f483c216f", size = 18437, upload-time = "2026-04-08T04:28:10.095Z" }, +] + +[[package]] +name = "types-pytz" +version = "2026.1.1.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/b7/33f5a4f29b1f285b99ff79a607751a7996194cbb98705e331dab7a2daa28/types_pytz-2026.1.1.20260408.tar.gz", hash = "sha256:89b6a34b9198ea2a4b98a9d15cbca987053f52a105fd44f7ce3789cae4349408", size = 10788, upload-time = "2026-04-08T04:28:14.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/90/12c059e6bb330a22d9cc97daf027ac7fb7f50fbf518e4d88185b4d39120e/types_pytz-2026.1.1.20260408-py3-none-any.whl", hash = "sha256:c7e4dec76221fb7d0c97b91ad8561d689bebe39b6bcb7b728387e7ffd8cde788", size = 10124, upload-time = "2026-04-08T04:28:13.353Z" }, +] + +[[package]] +name = "types-redis" +version = "4.6.0.20241004" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "types-pyopenssl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/95/c054d3ac940e8bac4ca216470c80c26688a0e79e09f520a942bb27da3386/types-redis-4.6.0.20241004.tar.gz", hash = "sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e", size = 49679, upload-time = "2024-10-04T02:43:59.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/82/7d25dce10aad92d2226b269bce2f85cfd843b4477cd50245d7d40ecf8f89/types_redis-4.6.0.20241004-py3-none-any.whl", hash = "sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed", size = 58737, upload-time = "2024-10-04T02:43:57.968Z" }, +] + +[[package]] +name = "types-requests" +version = "2.33.0.20260408" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/6a/749dc53a54a3f35842c1f8197b3ca6b54af6d7458a1bfc75f6629b6da666/types_requests-2.33.0.20260408.tar.gz", hash = "sha256:95b9a86376807a216b2fb412b47617b202091c3ea7c078f47cc358d5528ccb7b", size = 23882, upload-time = "2026-04-08T04:34:49.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/b8/78fd6c037de4788c040fdd323b3369804400351b7827473920f6c1d03c10/types_requests-2.33.0.20260408-py3-none-any.whl", hash = "sha256:81f31d5ea4acb39f03be7bc8bed569ba6d5a9c5d97e89f45ac43d819b68ca50f", size = 20739, upload-time = "2026-04-08T04:34:48.325Z" }, +] + +[[package]] +name = "types-setuptools" +version = "82.0.0.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/12/3464b410c50420dd4674fa5fe9d3880711c1dbe1a06f5fe4960ee9067b9e/types_setuptools-82.0.0.20260408.tar.gz", hash = "sha256:036c68caf7e672a699f5ebbf914708d40644c14e05298bc49f7272be91cf43d3", size = 44861, upload-time = "2026-04-08T04:29:33.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/e1/46a4fc3ef03aabf5d18bac9df5cf37c6b02c3bddf3e05c3533f4b4588331/types_setuptools-82.0.0.20260408-py3-none-any.whl", hash = "sha256:ece0a215cdfa6463a65fd6f68bd940f39e455729300ddfe61cab1147ed1d2462", size = 68428, upload-time = "2026-04-08T04:29:32.175Z" }, +] + +[[package]] +name = "types-tzlocal" +version = "5.1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/cf/e4d446e57c0b14ed1da4de180d2a4cac773b667f183e83bdad76ea6e2238/types-tzlocal-5.1.0.1.tar.gz", hash = "sha256:b84a115c0c68f0d0fa9af1c57f0645eeef0e539147806faf1f95ac3ac01ce47b", size = 3549, upload-time = "2023-10-24T02:15:07.127Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/13/caeb438290df069ddda6f055d0eb14337ada293c7d43ab89419ba4b1a778/types_tzlocal-5.1.0.1-py3-none-any.whl", hash = "sha256:0302e8067c86936de8f7e0aaedc2cfbf240080802c603df0f80312fbd4efb926", size = 3005, upload-time = "2023-10-24T02:15:05.815Z" }, +] + +[[package]] +name = "types-werkzeug" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/43/161261d2ac1fc20e944aa108e48a98ff0d994e19b498d6fb19d6637caf05/types-Werkzeug-1.0.9.tar.gz", hash = "sha256:5cc269604c400133d452a40cee6397655f878fc460e03fde291b9e3a5eaa518c", size = 23909, upload-time = "2021-11-26T06:21:28.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/c1/eaf8426126eafa46d649afb2fddede327043fbc2e84021b8b09a7fa15115/types_Werkzeug-1.0.9-py3-none-any.whl", hash = "sha256:194bd5715a13c598f05c63e8a739328657590943bce941e8a3619a6b5d4a54ec", size = 36186, upload-time = "2021-11-26T06:21:27.37Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" }, +] + +[[package]] +name = "u8darts" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "holidays" }, + { name = "joblib" }, + { name = "matplotlib" }, + { name = "narwhals" }, + { name = "nfoursid" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "pyod" }, + { name = "requests" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "shap" }, + { name = "statsmodels" }, + { name = "tqdm" }, + { name = "typing-extensions" }, + { name = "xarray" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/27/abd15f44b38af000829a0fb88ff414241cae3cfbb8b5871b5594c13e1fa3/u8darts-0.40.0.tar.gz", hash = "sha256:a907227f00ecb33702f7694ec85975e7c7a9bae9ee6ad6105b79c921f5ef6342", size = 977805, upload-time = "2025-12-23T15:56:28.97Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/b8/9dc177bedfba0349f289f6d970106307cbf9fc46d018b6c1e696cd9dabe2/u8darts-0.40.0-py3-none-any.whl", hash = "sha256:c4e6b97e2fe2f5cc2bbfc686a906544bdac05fdbb6a277cbe4a01ccc9628e4a1", size = 1134514, upload-time = "2025-12-23T15:56:27.236Z" }, +] + +[[package]] +name = "uniplot" +version = "0.21.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "readchar" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/66/198a3921ad6fd3c5e4de1e8d6ef4d9781d6c992e36f14bb5325717c0c8df/uniplot-0.21.5.tar.gz", hash = "sha256:24adc3c44b6719f3acd28cc8cac4afa621a0ff63f6dce3f8c8615c27923edb18", size = 35593, upload-time = "2026-01-04T12:40:29.333Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/e7/b3cd93b172b4e24f132af7e235623f68e704fd09b828822461eca0e98066/uniplot-0.21.5-py3-none-any.whl", hash = "sha256:7ba1bcef43376991fa1936c85186b08ce46065a63068ce0735df491c76e001f1", size = 37346, upload-time = "2026-01-04T12:40:28.269Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/98/3a7e644e19cb26133488caff231be390579860bbbb3da35913c49a1d0a46/virtualenv-21.2.4.tar.gz", hash = "sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada", size = 5850742, upload-time = "2026-04-14T22:15:31.438Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl", hash = "sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac", size = 5831232, upload-time = "2026-04-14T22:15:29.342Z" }, +] + +[[package]] +name = "vl-convert-python" +version = "1.9.0.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/93/89/36722344d1758ec2106f4e8eca980f173cfe8f8d0358c1b77cc5d2e035a4/vl_convert_python-1.9.0.post1.tar.gz", hash = "sha256:a5b06b3128037519001166f5341ec7831e19fbd7f3a5f78f73d557ac2d5859ef", size = 4663469, upload-time = "2026-01-21T00:09:55.61Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/59/e5862245972ff467d38b0eb5ad28154685e23ecabb47e14f2b6962da7b56/vl_convert_python-1.9.0.post1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:43e9515f65bbcd317d1ef328787fd7bf0344c2fde9292eb7a0e64d5d3d29fccb", size = 30512930, upload-time = "2026-01-21T00:09:43.198Z" }, + { url = "https://files.pythonhosted.org/packages/62/e6/e7d0b538c2f0daaf120901dc113bd5d5d1fa51a9532fa5ffd90234e8c69e/vl_convert_python-1.9.0.post1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:b0e7a3245f32addec7e7abeb1badf72b1513ed71ba1dba7aca853901217b3f4e", size = 29738742, upload-time = "2026-01-21T00:09:46.016Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e2/5645a1bc174c53ff8cd305ed76a4a76ba36e155302db20b42b7e78daeef8/vl_convert_python-1.9.0.post1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6ecfe4b7e2ea9e8c30fd6d6eaea3ef85475be1ad249407d9796dce4ecdb5b32", size = 33366278, upload-time = "2026-01-21T00:09:48.42Z" }, + { url = "https://files.pythonhosted.org/packages/a0/18/88e02899b72fa8273ffb32bde12b0e5776ee0fd9fb29559a49c48ec4c5fa/vl_convert_python-1.9.0.post1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c1558fa0055e88c465bd3d71760cde9fa2c94a95f776a0ef9178252fd820b1f", size = 33520215, upload-time = "2026-01-21T00:09:50.992Z" }, + { url = "https://files.pythonhosted.org/packages/2f/db/6e8616587035bf0745d0f10b1791c7e945180ac5d6b28677d2f2b3ca693c/vl_convert_python-1.9.0.post1-cp37-abi3-win_amd64.whl", hash = "sha256:7e263269ac0d304640ca842b44dfe430ed863accd9edecff42e279bfc48ce940", size = 32051516, upload-time = "2026-01-21T00:09:53.47Z" }, +] + +[[package]] +name = "webargs" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "marshmallow" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/64/17afc4e6f47eef154a553c6e56adcc9f1ac3003305c7df978d11aa62937e/webargs-8.7.1.tar.gz", hash = "sha256:799bf9039c76c23fd8dc1951107a75a9e561203c15d6ae8f89c1e46e234636c1", size = 97351, upload-time = "2025-10-29T16:07:50.066Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/ef/b0d17f3943429358184449771b592e0e1d33bbeaa6ed326434a95eac187b/webargs-8.7.1-py3-none-any.whl", hash = "sha256:a184aed9d2509e6e14ab99ee3e9dc3a614c7070affe94cd4dfdb0d002e0a6e5f", size = 32500, upload-time = "2025-10-29T16:07:47.895Z" }, +] + +[[package]] +name = "webauthn" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cbor2" }, + { name = "cryptography" }, + { name = "pyasn1" }, + { name = "pyopenssl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/f4/9529bcf85ef46c76842b84c66ffa8ec31f18e3aacd1330b62f440077b45b/webauthn-2.7.1.tar.gz", hash = "sha256:2a1ebbfffc4a83e31d3db5d69113944bc49d05fae77770c2d4e388386cb9656e", size = 124256, upload-time = "2026-02-11T23:36:02.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/5b/f73513367a9d34b199de916b44306acfa4027b57f7e22200421212b1f763/webauthn-2.7.1-py3-none-any.whl", hash = "sha256:d57e9613c65e0c6a4db7ee715fb49ebdf3c4a6eb3979729eeb497c99105e8181", size = 71684, upload-time = "2026-02-11T23:36:00.864Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, +] + +[[package]] +name = "workalendar" +version = "17.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "convertdate" }, + { name = "lunardate" }, + { name = "pyluach" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/16/e2d59c5a3b2d01519778dc1820daab70054022d88ff8291df602b2620481/workalendar-17.0.0.tar.gz", hash = "sha256:b82d6024aed452505b01baf06dbe8d6309a3135ff1d39dee07c31b21ece853b4", size = 153468, upload-time = "2023-01-01T22:33:06.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/23/2fd9e240ae95be5653d57cc39d5377278eb376b4b3f30e27526d831b1668/workalendar-17.0.0-py3-none-any.whl", hash = "sha256:8fe1d6758f9a1af05d59cb81480afb912246ec7fa46e246b909897127cdbb1b0", size = 210652, upload-time = "2023-01-01T22:33:03.869Z" }, +] + +[[package]] +name = "wrapt" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" }, + { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" }, + { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" }, + { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" }, + { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" }, + { url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259, upload-time = "2026-03-06T02:53:41.922Z" }, + { url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851, upload-time = "2026-03-06T02:52:48.672Z" }, + { url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446, upload-time = "2026-03-06T02:54:14.013Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056, upload-time = "2026-03-06T02:54:10.829Z" }, + { url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359, upload-time = "2026-03-06T02:53:03.623Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479, upload-time = "2026-03-06T02:53:45.844Z" }, + { url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271, upload-time = "2026-03-06T02:54:46.356Z" }, + { url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573, upload-time = "2026-03-06T02:52:50.163Z" }, + { url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205, upload-time = "2026-03-06T02:53:47.494Z" }, + { url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452, upload-time = "2026-03-06T02:53:30.038Z" }, + { url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842, upload-time = "2026-03-06T02:52:52.114Z" }, + { url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075, upload-time = "2026-03-06T02:53:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719, upload-time = "2026-03-06T02:54:33.452Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643, upload-time = "2026-03-06T02:52:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805, upload-time = "2026-03-06T02:54:25.441Z" }, + { url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990, upload-time = "2026-03-06T02:53:57.456Z" }, + { url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670, upload-time = "2026-03-06T02:54:55.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357, upload-time = "2026-03-06T02:54:24.092Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269, upload-time = "2026-03-06T02:53:31.268Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" }, + { url = "https://files.pythonhosted.org/packages/39/25/e7ea0b417db02bb796182a5316398a75792cd9a22528783d868755e1f669/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9", size = 61418, upload-time = "2026-03-06T02:53:55.706Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0f/fa539e2f6a770249907757eaeb9a5ff4deb41c026f8466c1c6d799088a9b/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9", size = 61914, upload-time = "2026-03-06T02:52:53.37Z" }, + { url = "https://files.pythonhosted.org/packages/53/37/02af1867f5b1441aaeda9c82deed061b7cd1372572ddcd717f6df90b5e93/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e", size = 120417, upload-time = "2026-03-06T02:54:30.74Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b7/0138a6238c8ba7476c77cf786a807f871672b37f37a422970342308276e7/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c", size = 122797, upload-time = "2026-03-06T02:54:51.539Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ad/819ae558036d6a15b7ed290d5b14e209ca795dd4da9c58e50c067d5927b0/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a", size = 117350, upload-time = "2026-03-06T02:54:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/8b/2d/afc18dc57a4600a6e594f77a9ae09db54f55ba455440a54886694a84c71b/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90", size = 121223, upload-time = "2026-03-06T02:54:35.221Z" }, + { url = "https://files.pythonhosted.org/packages/b9/5b/5ec189b22205697bc56eb3b62aed87a1e0423e9c8285d0781c7a83170d15/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586", size = 116287, upload-time = "2026-03-06T02:54:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2d/f84939a7c9b5e6cdd8a8d0f6a26cabf36a0f7e468b967720e8b0cd2bdf69/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19", size = 119593, upload-time = "2026-03-06T02:54:16.697Z" }, + { url = "https://files.pythonhosted.org/packages/0b/fe/ccd22a1263159c4ac811ab9374c061bcb4a702773f6e06e38de5f81a1bdc/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508", size = 58631, upload-time = "2026-03-06T02:53:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/65/0a/6bd83be7bff2e7efaac7b4ac9748da9d75a34634bbbbc8ad077d527146df/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04", size = 60875, upload-time = "2026-03-06T02:53:50.252Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c0/0b3056397fe02ff80e5a5d72d627c11eb885d1ca78e71b1a5c1e8c7d45de/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575", size = 59164, upload-time = "2026-03-06T02:53:59.128Z" }, + { url = "https://files.pythonhosted.org/packages/71/ed/5d89c798741993b2371396eb9d4634f009ff1ad8a6c78d366fe2883ea7a6/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb", size = 63163, upload-time = "2026-03-06T02:52:54.873Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8c/05d277d182bf36b0a13d6bd393ed1dec3468a25b59d01fba2dd70fe4d6ae/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22", size = 63723, upload-time = "2026-03-06T02:52:56.374Z" }, + { url = "https://files.pythonhosted.org/packages/f4/27/6c51ec1eff4413c57e72d6106bb8dec6f0c7cdba6503d78f0fa98767bcc9/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596", size = 152652, upload-time = "2026-03-06T02:53:23.79Z" }, + { url = "https://files.pythonhosted.org/packages/db/4c/d7dd662d6963fc7335bfe29d512b02b71cdfa23eeca7ab3ac74a67505deb/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044", size = 158807, upload-time = "2026-03-06T02:53:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4d/1e5eea1a78d539d346765727422976676615814029522c76b87a95f6bcdd/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b", size = 146061, upload-time = "2026-03-06T02:52:57.574Z" }, + { url = "https://files.pythonhosted.org/packages/89/bc/62cabea7695cd12a288023251eeefdcb8465056ddaab6227cb78a2de005b/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf", size = 155667, upload-time = "2026-03-06T02:53:39.422Z" }, + { url = "https://files.pythonhosted.org/packages/e9/99/6f2888cd68588f24df3a76572c69c2de28287acb9e1972bf0c83ce97dbc1/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2", size = 144392, upload-time = "2026-03-06T02:54:22.41Z" }, + { url = "https://files.pythonhosted.org/packages/40/51/1dfc783a6c57971614c48e361a82ca3b6da9055879952587bc99fe1a7171/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3", size = 150296, upload-time = "2026-03-06T02:54:07.848Z" }, + { url = "https://files.pythonhosted.org/packages/6c/38/cbb8b933a0201076c1f64fc42883b0023002bdc14a4964219154e6ff3350/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7", size = 60539, upload-time = "2026-03-06T02:54:00.594Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/e5176e4b241c9f528402cebb238a36785a628179d7d8b71091154b3e4c9e/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5", size = 63969, upload-time = "2026-03-06T02:54:39Z" }, + { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, +] + +[[package]] +name = "wtforms" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/e4/633d080897e769ed5712dcfad626e55dbd6cf45db0ff4d9884315c6a82da/wtforms-3.2.1.tar.gz", hash = "sha256:df3e6b70f3192e92623128123ec8dca3067df9cfadd43d59681e210cfb8d4682", size = 137801, upload-time = "2024-10-21T11:34:00.108Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/c9/2088fb5645cd289c99ebe0d4cdcc723922a1d8e1beaefb0f6f76dff9b21c/wtforms-3.2.1-py3-none-any.whl", hash = "sha256:583bad77ba1dd7286463f21e11aa3043ca4869d03575921d1a1698d0715e0fd4", size = 152454, upload-time = "2024-10-21T11:33:58.44Z" }, +] + +[[package]] +name = "xarray" +version = "2026.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/a6/6fe936a798a3a38a79c7422d1a31afd2e9a14690fcb0ccff96bc01f04bf2/xarray-2026.4.0.tar.gz", hash = "sha256:c4ac9a01a945d90d5b1628e2af045099a9d4943536d4f2ee3ae963c3b222d15b", size = 3132311, upload-time = "2026-04-13T19:45:36.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/83/6d810a8a9ebc9c307989b418840c20e46907c74d707beb67ab566773e6fc/xarray-2026.4.0-py3-none-any.whl", hash = "sha256:d43751d9fb4a90f9249c30431684f00c41bc874f1edccd862631a40cbc0edf08", size = 1414326, upload-time = "2026-04-13T19:45:34.659Z" }, +] + +[[package]] +name = "xlrd" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/5a/377161c2d3538d1990d7af382c79f3b2372e880b65de21b01b1a2b78691e/xlrd-2.0.2.tar.gz", hash = "sha256:08b5e25de58f21ce71dc7db3b3b8106c1fa776f3024c54e45b45b374e89234c9", size = 100167, upload-time = "2025-06-14T08:46:39.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/62/c8d562e7766786ba6587d09c5a8ba9f718ed3fa8af7f4553e8f91c36f302/xlrd-2.0.2-py2.py3-none-any.whl", hash = "sha256:ea762c3d29f4cca48d82df517b6d89fbce4db3107f9d78713e48cd321d5c9aa9", size = 96555, upload-time = "2025-06-14T08:46:37.766Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, +] From 016ec7f06fd4efc002ea92ad15ad9bc9503911b2 Mon Sep 17 00:00:00 2001 From: Felix Claessen <30658763+Flix6x@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:05:16 +0200 Subject: [PATCH 02/13] chore: upgrade minimum version to FM v0.33 Signed-off-by: Felix Claessen <30658763+Flix6x@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 801bb2c..7bae123 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ ] version = "0.1" dependencies = [ - "flexmeasures>=0.32.0", + "flexmeasures>=0.33.0", # introduces the ingestion queue "openadr3-client>=2.0.4", ] From 86eef1e72474a1a96fd572c6569b054959e18a15 Mon Sep 17 00:00:00 2001 From: Nick van der Burgt Date: Mon, 22 Jun 2026 10:10:00 +0200 Subject: [PATCH 03/13] Use SHA hashes for github actions, use ingestion queue, add pytest speedup to config --- .github/workflows/lint-and-test.yml | 8 ++++---- flexmeasures_openadr3/utils/ven_jobs.py | 2 +- pyproject.toml | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index 24be9e6..0032d65 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -8,11 +8,11 @@ jobs: runs-on: ubuntu-latest name: "Check (on Python3.11)" steps: - - uses: actions/setup-python@v4 + - uses: actions/setup-python@d09bd5e6005b175076f227b13d9730d56e9dcfcb with: python-version: 3.11 - uses: actions/checkout@v3 - - uses: pre-commit/action@v3.0.0 + - uses: pre-commit/action@646c83fcd040023954eafda54b4db0192ce70507 test: needs: check @@ -21,9 +21,9 @@ jobs: fail-fast: false name: "Test (on Python 3.11)" steps: - - uses: actions/setup-python@v4 + - uses: actions/setup-python@d09bd5e6005b175076f227b13d9730d56e9dcfcb with: python-version: 3.11 - name: Check out src from Git - uses: actions/checkout@v3 + uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 - run: make test diff --git a/flexmeasures_openadr3/utils/ven_jobs.py b/flexmeasures_openadr3/utils/ven_jobs.py index 51d8084..ebe3cae 100644 --- a/flexmeasures_openadr3/utils/ven_jobs.py +++ b/flexmeasures_openadr3/utils/ven_jobs.py @@ -25,7 +25,7 @@ OPENADR_EVENT_SOURCE_NAME = "OpenADR 3 VTN" OPENADR_EVENT_SOURCE_TYPE = "gateway" -FETCH_EVENTS_QUEUE_NAME = "forecasting" # "ingestion" +FETCH_EVENTS_QUEUE_NAME = "ingestion" SUPPORTED_DR_PAYLOAD_TYPES = ( EventPayloadType.IMPORT_CAPACITY_LIMIT, EventPayloadType.EXPORT_CAPACITY_LIMIT, diff --git a/pyproject.toml b/pyproject.toml index 7bae123..c121d2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,7 @@ dev-dependencies = [ [tool.pytest.ini_options] testpaths = ["tests"] pythonpath = ["."] +addopts = ["--import-mode=importlib"] [tool.mypy] python_version = "3.12" From b37c11a8f5d05174d53cb1be51fa515a39562121 Mon Sep 17 00:00:00 2001 From: Nick van der Burgt Date: Mon, 22 Jun 2026 13:03:04 +0200 Subject: [PATCH 04/13] Update openadr3-client to latest version --- pyproject.toml | 6 +- uv.lock | 251 ++++++++++++++++++++++--------------------------- 2 files changed, 118 insertions(+), 139 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c121d2f..c61bed2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,8 +25,8 @@ classifiers = [ ] version = "0.1" dependencies = [ - "flexmeasures>=0.33.0", # introduces the ingestion queue - "openadr3-client>=2.0.4", + "flexmeasures>=0.33.0", # introduces the ingestion queue + "openadr3-client==2.1.0", ] [project.urls] @@ -49,7 +49,7 @@ dev-dependencies = [ # Testing "pytest>=7.0.0", "pytest-cov>=4.0.0", - # Linting & formatting + # Linting & formatting "mypy>=1.0.0", "ruff>=0.15.11", # Pre-commit diff --git a/uv.lock b/uv.lock index abd71eb..a3771d5 100644 --- a/uv.lock +++ b/uv.lock @@ -703,6 +703,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] +[[package]] +name = "darts" +version = "0.45.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "holidays" }, + { name = "joblib" }, + { name = "matplotlib" }, + { name = "narwhals" }, + { name = "nfoursid" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "pyod" }, + { name = "requests" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "shap" }, + { name = "statsmodels" }, + { name = "tqdm" }, + { name = "typing-extensions" }, + { name = "xarray" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/b1/cae38a86011799c23ee6baa35c0345a34e3002e4ef0e0685e9a1947e41ff/darts-0.45.0.tar.gz", hash = "sha256:ae59113e56a9798a6f05cfb4c306c6d0d6f356d17dc215cb3956887d54539831", size = 690353, upload-time = "2026-06-19T14:50:34.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/f6/3c0fbd1c412e55ed644852b1f7f98fce7f6d9967933d266c69da7aba7e13/darts-0.45.0-py3-none-any.whl", hash = "sha256:f6e1571295c245fffc194fcae135e812a63af884ae5fbb517f6379d57357adfd", size = 808760, upload-time = "2026-06-19T14:50:32.873Z" }, +] + [[package]] name = "deprecation" version = "2.1.0" @@ -1001,7 +1028,7 @@ wheels = [ [[package]] name = "flexmeasures" -version = "0.32.2" +version = "0.33.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "altair" }, @@ -1013,6 +1040,7 @@ dependencies = [ { name = "click" }, { name = "click-default-group" }, { name = "cryptography" }, + { name = "darts" }, { name = "email-validator" }, { name = "flask" }, { name = "flask-classful" }, @@ -1057,9 +1085,7 @@ dependencies = [ { name = "sqlalchemy" }, { name = "tabulate" }, { name = "timely-beliefs", extra = ["forecast"] }, - { name = "timetomodel" }, { name = "tldextract" }, - { name = "u8darts" }, { name = "uniplot" }, { name = "urllib3" }, { name = "vl-convert-python" }, @@ -1068,9 +1094,9 @@ dependencies = [ { name = "workalendar" }, { name = "xlrd" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/00/e89562c7de0d96cb9620be5cbca7d4c004ab5544b7ccff23f50edcec5b24/flexmeasures-0.32.2.tar.gz", hash = "sha256:8888f63fc2b1ea2101784500d487e88f5e50bd74e2930a66d3ee6cfd247195c6", size = 2620032, upload-time = "2026-05-12T17:54:25.18Z" } +sdist = { url = "https://files.pythonhosted.org/packages/77/b8/1f7650ec5e0c3b41d03a884e27ffeb11070c163a9c50ffe1d06dec562164/flexmeasures-0.33.0.tar.gz", hash = "sha256:2d512adaaec2e46861564f6def91527ef42671cbcdc49715bc5af17b373cae8f", size = 2701191, upload-time = "2026-06-01T15:52:19.444Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/03/b1fc92c47b75af872c76eb4773e8bd9023998ae82fe171004b3786c362f0/flexmeasures-0.32.2-py3-none-any.whl", hash = "sha256:a5d0c4c4231db3ecec0031b65e46d98c632c2f3ce0eab30fea65dd229169f774", size = 2412646, upload-time = "2026-05-12T17:54:22.786Z" }, + { url = "https://files.pythonhosted.org/packages/14/df/6fbb0205d336bb97f2a334c8522ee8a05f095a777b641350b8a61bae9376/flexmeasures-0.33.0-py3-none-any.whl", hash = "sha256:ddf874cb434c82ad3a3387ac9d8c33cb6ac01e443cba36ad1d34b051656f4573", size = 2476457, upload-time = "2026-06-01T15:52:16.913Z" }, ] [[package]] @@ -1104,8 +1130,8 @@ dev = [ [package.metadata] requires-dist = [ - { name = "flexmeasures", specifier = ">=0.32.0" }, - { name = "openadr3-client", specifier = ">=2.0.4" }, + { name = "flexmeasures", specifier = ">=0.33.0" }, + { name = "openadr3-client", specifier = "==2.1.0" }, ] [package.metadata.requires-dev] @@ -2007,7 +2033,7 @@ signedtoken = [ [[package]] name = "openadr3-client" -version = "2.0.4" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "oauthlib", extra = ["signedtoken"] }, @@ -2017,9 +2043,9 @@ dependencies = [ { name = "requests" }, { name = "requests-oauthlib" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/9b/1187ed5d6d0db5fcb40a0a7fa5087755c9378a18aba033aad0251f67dfa5/openadr3_client-2.0.4.tar.gz", hash = "sha256:8671e96a152a25b19bb7b46bf955356376aef7c085393bab33fc2c45e95509d5", size = 63033, upload-time = "2026-03-16T10:01:37.593Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/3b/4471a967f73fe4b14c7f45aa63fb6d3e505866357c1715440324f148c91a/openadr3_client-2.1.0.tar.gz", hash = "sha256:dc653117d32e4ff7bdb1212edfcbcaaffba45dbe07b1651daf8356bca1c62f48", size = 64865, upload-time = "2026-06-01T11:15:49.86Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/3b/ee4fd55d343097d0fd8f27b590c936d57a31d78daa6e0da02641162334a3/openadr3_client-2.0.4-py3-none-any.whl", hash = "sha256:369de74ac26acbaf0732a2046d566ed5a3da692af807d388235ed2a240e6e945", size = 139814, upload-time = "2026-03-16T10:01:35.937Z" }, + { url = "https://files.pythonhosted.org/packages/36/3e/a6ebc84ed257e8b2cd72a0d42b4122a89de69ff995b56990557d299f02b4/openadr3_client-2.1.0-py3-none-any.whl", hash = "sha256:8c7b2f35a5c5960b467d32efec14e5161ea0b2c0fbad1ed1c70d373e7d77750d", size = 144045, upload-time = "2026-06-01T11:15:48.573Z" }, ] [[package]] @@ -2348,7 +2374,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.13.3" +version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -2356,84 +2382,84 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] [[package]] name = "pydantic-core" -version = "2.46.3" +version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" }, - { url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" }, - { url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" }, - { url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" }, - { url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" }, - { url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" }, - { url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" }, - { url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" }, - { url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" }, - { url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" }, - { url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" }, - { url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" }, - { url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" }, - { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" }, - { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" }, - { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" }, - { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" }, - { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" }, - { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" }, - { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" }, - { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" }, - { url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" }, - { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" }, - { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" }, - { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" }, - { url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" }, - { url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" }, - { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" }, - { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" }, - { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" }, - { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" }, - { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" }, - { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" }, - { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" }, - { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" }, - { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" }, - { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" }, - { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" }, - { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" }, - { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" }, - { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" }, - { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" }, - { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" }, - { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" }, - { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" }, - { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" }, - { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" }, - { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" }, - { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" }, - { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" }, - { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" }, - { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" }, - { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" }, - { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" }, - { url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" }, - { url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" }, - { url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" }, - { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, ] [[package]] @@ -2751,7 +2777,7 @@ wheels = [ [[package]] name = "requests" -version = "2.33.1" +version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -2759,9 +2785,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] @@ -2898,7 +2924,7 @@ wheels = [ [[package]] name = "rq-dashboard" -version = "0.8.6" +version = "0.8.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "arrow" }, @@ -2907,9 +2933,9 @@ dependencies = [ { name = "redis-sentinel-url" }, { name = "rq" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/74/01ad052d8aec0e0d31a9575bd4e19a0aa62235c0e8a259cf7fab12b1fda5/rq_dashboard-0.8.6.tar.gz", hash = "sha256:36a3182a6ea854642d444f8691bb4e692afb36394429cd8c2c4963fbc8cbb572", size = 210231, upload-time = "2025-12-02T08:36:01.354Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/73/2e4347593d50bed8dcf0ba89ac180133f04de056fc9f7c701cfd56988f9a/rq_dashboard-0.8.7.tar.gz", hash = "sha256:068964cd677787586724ae71d051c4c963fdd5ea86515f1b42357bf983253951", size = 210994, upload-time = "2026-05-26T05:06:08.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/5e/8b92e99341fd81f7349a5614756c3e3bfc1ec8cae6dbc4b45649ec07fac3/rq_dashboard-0.8.6-py2.py3-none-any.whl", hash = "sha256:dc7b3517538bfe95e4ac9fe3a3017af939f940da88a79edd5cf40f26dca4ce67", size = 213580, upload-time = "2025-12-02T08:35:59.942Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/c77dec616466b33b62240a89e1a71f5d88df3ebfe0ace6be2403b55a492a/rq_dashboard-0.8.7-py2.py3-none-any.whl", hash = "sha256:889e9c302251f7e95720360acff515a40a274d340675c74189bfa4ffbcd8c63f", size = 213975, upload-time = "2026-05-26T05:06:07.2Z" }, ] [[package]] @@ -3308,26 +3334,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/2f/811d8183abae998effbbb1a191af2469a6c18bfcb4d3edfd4fe9c096c5f8/times-0.7-py2.py3-none-any.whl", hash = "sha256:71a47e488fb727fecbd2bc6b4e3559fd4425eb6e20c68b1418069325a4c58a37", size = 3826, upload-time = "2014-08-24T06:13:21.84Z" }, ] -[[package]] -name = "timetomodel" -version = "0.7.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "matplotlib" }, - { name = "numpy" }, - { name = "pandas" }, - { name = "python-dateutil" }, - { name = "pytz" }, - { name = "scikit-learn" }, - { name = "scipy" }, - { name = "sqlalchemy" }, - { name = "statsmodels" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/51/948aa9b4e8498924181eb3d17533eff0b43483d46ca9201b6c96d24b410c/timetomodel-0.7.3.tar.gz", hash = "sha256:dd57e20f1fea58240d8a93f38d0d9f598fc0600e88eeac5dceecbf34a750e08c", size = 141996, upload-time = "2023-06-07T13:37:05.398Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/65/2d/379105ee6650157b650c5a0da42ccab698fae50687d3ee481a2532bb1ba6/timetomodel-0.7.3-py2.py3-none-any.whl", hash = "sha256:031f0d2cd8d6320c9a8dfd35cf3d976bdc4f0d74f9322690d6ad74d05445ff05", size = 29820, upload-time = "2023-06-07T13:37:03.356Z" }, -] - [[package]] name = "tldextract" version = "5.3.1" @@ -3565,33 +3571,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" }, ] -[[package]] -name = "u8darts" -version = "0.40.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "holidays" }, - { name = "joblib" }, - { name = "matplotlib" }, - { name = "narwhals" }, - { name = "nfoursid" }, - { name = "numpy" }, - { name = "pandas" }, - { name = "pyod" }, - { name = "requests" }, - { name = "scikit-learn" }, - { name = "scipy" }, - { name = "shap" }, - { name = "statsmodels" }, - { name = "tqdm" }, - { name = "typing-extensions" }, - { name = "xarray" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/27/abd15f44b38af000829a0fb88ff414241cae3cfbb8b5871b5594c13e1fa3/u8darts-0.40.0.tar.gz", hash = "sha256:a907227f00ecb33702f7694ec85975e7c7a9bae9ee6ad6105b79c921f5ef6342", size = 977805, upload-time = "2025-12-23T15:56:28.97Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/b8/9dc177bedfba0349f289f6d970106307cbf9fc46d018b6c1e696cd9dabe2/u8darts-0.40.0-py3-none-any.whl", hash = "sha256:c4e6b97e2fe2f5cc2bbfc686a906544bdac05fdbb6a277cbe4a01ccc9628e4a1", size = 1134514, upload-time = "2025-12-23T15:56:27.236Z" }, -] - [[package]] name = "uniplot" version = "0.21.5" @@ -3607,11 +3586,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] From 71ccea44a0e253e3d9c698f8d6fc22cc7ad8d76d Mon Sep 17 00:00:00 2001 From: Nick van der Burgt Date: Mon, 22 Jun 2026 13:06:55 +0200 Subject: [PATCH 05/13] rename function, move top level imports --- flexmeasures_openadr3/__init__.py | 9 ++++----- flexmeasures_openadr3/utils/ven_jobs.py | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/flexmeasures_openadr3/__init__.py b/flexmeasures_openadr3/__init__.py index 72167af..ed58a34 100644 --- a/flexmeasures_openadr3/__init__.py +++ b/flexmeasures_openadr3/__init__.py @@ -1,5 +1,9 @@ from __future__ import annotations +import warnings +from importlib_metadata import version as pkg_version +from packaging.version import Version + from dataclasses import dataclass from typing import TypedDict @@ -17,11 +21,6 @@ FlexMeasures registers the BluePrint objects it finds in here. """ - -import warnings # NOQA: E402 -from importlib_metadata import version as pkg_version # NOQA: E402 -from packaging.version import Version # NOQA: E402 - try: _fm_version = pkg_version("flexmeasures") if Version(_fm_version) < Version("v0.32.0"): diff --git a/flexmeasures_openadr3/utils/ven_jobs.py b/flexmeasures_openadr3/utils/ven_jobs.py index ebe3cae..ec59188 100644 --- a/flexmeasures_openadr3/utils/ven_jobs.py +++ b/flexmeasures_openadr3/utils/ven_jobs.py @@ -59,7 +59,7 @@ def from_sensor_config( ) return cls(import_sensor=import_sensor, export_sensor=export_sensor) - def sensor_for(self, payload_type: EventPayloadType) -> Sensor | None: + def get_sensor_for(self, payload_type: EventPayloadType) -> Sensor | None: if payload_type is EventPayloadType.IMPORT_CAPACITY_LIMIT: return self.import_sensor if payload_type is EventPayloadType.EXPORT_CAPACITY_LIMIT: @@ -182,7 +182,7 @@ def _store_event_payloads( for interval in event.intervals or (): start, end = _get_interval_period(event, interval) for payload in interval.payloads: - sensor = sensor_mapping.sensor_for(payload.type) + sensor = sensor_mapping.get_sensor_for(payload.type) if sensor is None: continue if len(payload.values) != 1: From dab3a6d9b639cd4c3a254a214144cc44e2bcac5f Mon Sep 17 00:00:00 2001 From: Nick van der Burgt Date: Tue, 23 Jun 2026 10:11:35 +0200 Subject: [PATCH 06/13] Fix mypy, ruff errors --- conftest.py | 14 +- .../python/__init__.py | 0 .../python/seed_events.py | 21 +-- .../python/settings.py | 8 +- .../python/trigger_fetch.py | 13 +- flexmeasures_openadr3/__init__.py | 22 +-- flexmeasures_openadr3/models/__init__.py | 2 +- flexmeasures_openadr3/models/forms.py | 45 +++-- flexmeasures_openadr3/models/jobs.py | 7 +- flexmeasures_openadr3/models/storage.py | 33 ++-- flexmeasures_openadr3/py.typed | 1 - flexmeasures_openadr3/ui/views/dashboard.py | 145 +++++++--------- flexmeasures_openadr3/utils/blueprints.py | 3 +- flexmeasures_openadr3/utils/encryption.py | 25 +-- flexmeasures_openadr3/utils/events.py | 20 +-- flexmeasures_openadr3/utils/sensor.py | 144 +++++++--------- flexmeasures_openadr3/utils/ven_clients.py | 161 ++++++++---------- flexmeasures_openadr3/utils/ven_jobs.py | 80 ++++----- pyproject.toml | 40 ++++- tests/conftest.py | 57 +++---- tests/test_container/conftest.py | 35 ++-- .../flexmeasures_test_postgres_container.py | 13 +- tests/test_container/integration_types.py | 6 + .../oadr310_vtn_test_container.py | 30 +--- tests/test_container/openadr_vtn_setup.py | 9 +- tests/test_ven_client_repository.py | 24 +-- tests/test_ven_fetch_e2e.py | 42 +---- tests/test_ven_fetch_jobs.py | 13 +- 28 files changed, 435 insertions(+), 578 deletions(-) create mode 100644 examples/capacity-limits-ingestion-walkthrough/python/__init__.py diff --git a/conftest.py b/conftest.py index 5a24b3f..9aa7562 100644 --- a/conftest.py +++ b/conftest.py @@ -3,11 +3,10 @@ from __future__ import annotations import os -from collections.abc import Iterator +from typing import TYPE_CHECKING import pytest from flask import Flask - from flexmeasures.app import create as create_flexmeasures_app from tests.test_container.flexmeasures_test_postgres_container import ( @@ -15,6 +14,9 @@ FlexMeasuresTestPostgresContainer, ) +if TYPE_CHECKING: + from collections.abc import Iterator + pytest_plugins = [ "flexmeasures.conftest", ] @@ -22,9 +24,7 @@ @pytest.fixture(scope="session") def integration_test_flexmeasures_postgres() -> Iterator[FlexMeasuresTestPostgres]: - """ - Session-scoped PostgreSQL testcontainer for FlexMeasures during integration tests. - """ + """Session-scoped PostgreSQL testcontainer for FlexMeasures during integration tests.""" with FlexMeasuresTestPostgresContainer() as postgres_container: yield postgres_container.get_connection_info() @@ -37,9 +37,7 @@ def app( # FlexMeasures reads this env var during create_app() (see read_config in testing mode). # Setting SQLALCHEMY_DATABASE_URI on the app config afterwards is too late: db.init_app() # already created a cached engine bound to TestingConfig's localhost default. - os.environ["SQLALCHEMY_TEST_DATABASE_URI"] = ( - integration_test_flexmeasures_postgres.database_uri - ) + os.environ["SQLALCHEMY_TEST_DATABASE_URI"] = integration_test_flexmeasures_postgres.database_uri test_app = create_flexmeasures_app( env="testing", diff --git a/examples/capacity-limits-ingestion-walkthrough/python/__init__.py b/examples/capacity-limits-ingestion-walkthrough/python/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/capacity-limits-ingestion-walkthrough/python/seed_events.py b/examples/capacity-limits-ingestion-walkthrough/python/seed_events.py index ea5eed0..7e57e5a 100644 --- a/examples/capacity-limits-ingestion-walkthrough/python/seed_events.py +++ b/examples/capacity-limits-ingestion-walkthrough/python/seed_events.py @@ -12,8 +12,7 @@ import random import sys import uuid -from datetime import datetime, timedelta, timezone -from typing import Tuple +from datetime import UTC, datetime, timedelta from openadr3_client._models.common.interval_period import IntervalPeriod from openadr3_client.bl.http_factory import BusinessLogicHttpClientFactory @@ -27,7 +26,6 @@ from openadr3_client.oadr310.models.program.program import NewProgram from openadr3_client.oadr310.models.unit import Unit from openadr3_client.version import OADRVersion - from settings import ( BL_OAUTH_CLIENT_ID, HOST_KEYCLOAK_TOKEN_URL, @@ -64,7 +62,7 @@ def seed_capacity_limit_event( The event starts one hour from the current UTC time and spans 24 hours as 96 consecutive 15-minute intervals — matching tests/test_container/openadr_vtn_setup.py. """ - event_start = datetime.now(timezone.utc).replace(microsecond=0) + timedelta(hours=1) + event_start = datetime.now(UTC).replace(microsecond=0) + timedelta(hours=1) event_duration = timedelta(hours=24) payload_descriptors = ( EventPayloadDescriptor( @@ -84,7 +82,7 @@ def seed_capacity_limit_event( ) ) - intervals: Tuple[Interval[EventPayload], ...] = () + intervals: tuple[Interval[EventPayload], ...] = () for i in range(event_duration // timedelta(minutes=15)): intervals += ( Interval( @@ -118,19 +116,12 @@ def seed_capacity_limit_event( def main() -> None: + """Seed a capacity-limit event on the VTN and print a short summary.""" print("Connecting to VTN at", HOST_VTN_BASE_URL) bl_client = create_bl_client() event = seed_capacity_limit_event(bl_client) - first_start = ( - event.intervals[0].interval_period.start - if event.intervals and event.intervals[0].interval_period - else None - ) - last_start = ( - event.intervals[-1].interval_period.start - if event.intervals and event.intervals[-1].interval_period - else None - ) + first_start = event.intervals[0].interval_period.start if event.intervals and event.intervals[0].interval_period else None + last_start = event.intervals[-1].interval_period.start if event.intervals and event.intervals[-1].interval_period else None print(f"Created event id={event.id} with {len(event.intervals or ())} intervals.") if first_start and last_start: print(f" First interval start (UTC): {first_start.isoformat()}") diff --git a/examples/capacity-limits-ingestion-walkthrough/python/settings.py b/examples/capacity-limits-ingestion-walkthrough/python/settings.py index f4921ba..41ab675 100644 --- a/examples/capacity-limits-ingestion-walkthrough/python/settings.py +++ b/examples/capacity-limits-ingestion-walkthrough/python/settings.py @@ -8,16 +8,12 @@ OAUTH_CLIENT_SECRET = "my-client-secret" # URLs when running Python scripts on your host machine -HOST_KEYCLOAK_TOKEN_URL = ( - f"http://localhost:8080/realms/{KEYCLOAK_REALM}/protocol/openid-connect/token" -) +HOST_KEYCLOAK_TOKEN_URL = f"http://localhost:8080/realms/{KEYCLOAK_REALM}/protocol/openid-connect/token" HOST_VTN_BASE_URL = "http://localhost:3000" # URLs to enter in the FlexMeasures UI (resolved inside the Docker network) DOCKER_VTN_BASE_URL = "http://openleadr-vtn:3000" -DOCKER_KEYCLOAK_TOKEN_URL = ( - f"http://keycloak:8080/realms/{KEYCLOAK_REALM}/protocol/openid-connect/token" -) +DOCKER_KEYCLOAK_TOKEN_URL = f"http://keycloak:8080/realms/{KEYCLOAK_REALM}/protocol/openid-connect/token" # Suggested names for the UI walkthrough VEN_CLIENT_NAME = "demo-ven" diff --git a/examples/capacity-limits-ingestion-walkthrough/python/trigger_fetch.py b/examples/capacity-limits-ingestion-walkthrough/python/trigger_fetch.py index 98667bd..8860894 100644 --- a/examples/capacity-limits-ingestion-walkthrough/python/trigger_fetch.py +++ b/examples/capacity-limits-ingestion-walkthrough/python/trigger_fetch.py @@ -14,16 +14,15 @@ import sys from flexmeasures.app import create as create_flexmeasures_app +from settings import POLLING_SCHEDULE_NAME, VEN_CLIENT_NAME + from flexmeasures_openadr3.utils.ven_clients import VenClientRepository from flexmeasures_openadr3.utils.ven_jobs import VenFetchJobScheduler -from settings import POLLING_SCHEDULE_NAME, VEN_CLIENT_NAME - def main() -> None: - parser = argparse.ArgumentParser( - description="Trigger an immediate OpenADR event fetch for a polling schedule." - ) + """Trigger an immediate OpenADR event fetch for a configured polling schedule.""" + parser = argparse.ArgumentParser(description="Trigger an immediate OpenADR event fetch for a polling schedule.") parser.add_argument( "--ven-name", default=VEN_CLIENT_NAME, @@ -55,9 +54,7 @@ def main() -> None: raise SystemExit(1) scheduler._execute(ven_client.id, config.name) - print( - f"Fetch complete for VEN '{args.ven_name}' schedule '{args.config_name}'." - ) + print(f"Fetch complete for VEN '{args.ven_name}' schedule '{args.config_name}'.") if __name__ == "__main__": diff --git a/flexmeasures_openadr3/__init__.py b/flexmeasures_openadr3/__init__.py index ed58a34..f2c7f2e 100644 --- a/flexmeasures_openadr3/__init__.py +++ b/flexmeasures_openadr3/__init__.py @@ -1,14 +1,14 @@ from __future__ import annotations import warnings -from importlib_metadata import version as pkg_version -from packaging.version import Version - from dataclasses import dataclass from typing import TypedDict from flask import Blueprint from flask.blueprints import BlueprintSetupState +from importlib_metadata import PackageNotFoundError +from importlib_metadata import version as pkg_version +from packaging.version import Version from .utils.blueprints import ensure_bp_routes_are_loaded_fresh @@ -24,15 +24,14 @@ try: _fm_version = pkg_version("flexmeasures") if Version(_fm_version) < Version("v0.32.0"): - warnings.warn( - f"flexmeasures-openadr3 requires FlexMeasures >= v0.32.0, " - f"but version {_fm_version} is installed." - ) -except Exception: + warnings.warn(f"flexmeasures-openadr3 requires FlexMeasures >= v0.32.0, but version {_fm_version} is installed.", stacklevel=2) +except PackageNotFoundError: pass class PluginSettingSpec(TypedDict): + """FlexMeasures plugin setting metadata exposed to administrators.""" + description: str level: str required: bool @@ -67,6 +66,8 @@ class PluginSettingSpec(TypedDict): @dataclass(frozen=True, slots=True) class MenuRegistration: + """Sidebar menu entry registered with FlexMeasures.""" + view_key: str title: str icon: str @@ -81,11 +82,10 @@ class MenuRegistration: @flexmeasures_openadr3_ui_bp.record_once def register_menu_item(setup_state: BlueprintSetupState) -> None: + """Register the OpenADR 3 dashboard in the FlexMeasures sidebar menu.""" app = setup_state.app registration = OPENADR_MENU_REGISTRATION - app.config["FLEXMEASURES_MENU_LISTED_VIEWS"] = app.config.get( - "FLEXMEASURES_MENU_LISTED_VIEWS", [] - ) + [registration.view_key] + app.config["FLEXMEASURES_MENU_LISTED_VIEWS"] = [*app.config.get("FLEXMEASURES_MENU_LISTED_VIEWS", []), registration.view_key] app.config["FLEXMEASURES_MENU_LISTED_VIEW_TITLES"] = { **app.config.get("FLEXMEASURES_MENU_LISTED_VIEW_TITLES", {}), registration.view_key: registration.title, diff --git a/flexmeasures_openadr3/models/__init__.py b/flexmeasures_openadr3/models/__init__.py index 7b6e5c2..a10e520 100644 --- a/flexmeasures_openadr3/models/__init__.py +++ b/flexmeasures_openadr3/models/__init__.py @@ -14,10 +14,10 @@ from flexmeasures_openadr3.models.views import VenSensorConfigOverview __all__ = [ + "VEN_CLIENT_ATTRIBUTE_KEY", "EventActivePeriod", "FormValidationErrors", "FormValidationResult", - "VEN_CLIENT_ATTRIBUTE_KEY", "VenClientAttributePayload", "VenClientFormValues", "VenSensorConfigFormValues", diff --git a/flexmeasures_openadr3/models/forms.py b/flexmeasures_openadr3/models/forms.py index c02119f..59f994a 100644 --- a/flexmeasures_openadr3/models/forms.py +++ b/flexmeasures_openadr3/models/forms.py @@ -1,8 +1,10 @@ from __future__ import annotations -from collections.abc import Iterator, Mapping from dataclasses import dataclass, field, replace -from typing import Generic, TypeVar +from typing import TYPE_CHECKING, TypeVar + +if TYPE_CHECKING: + from collections.abc import Iterator, Mapping ValidatedDataT = TypeVar("ValidatedDataT") @@ -14,34 +16,44 @@ class FormValidationErrors: _errors: dict[str, str] = field(default_factory=dict) def add(self, field_name: str, message: str) -> None: + """Record a validation error for a form field.""" if field_name not in self._errors: self._errors[field_name] = message def __getattr__(self, name: str) -> str: + """Return the error message for a field, or an empty string.""" return self._errors.get(name, "") def __bool__(self) -> bool: + """Return whether any validation errors are present.""" return bool(self._errors) def __len__(self) -> int: + """Return the number of validation errors.""" return len(self._errors) def __iter__(self) -> Iterator[tuple[str, str]]: + """Iterate over field names and their error messages.""" return iter(self._errors.items()) @dataclass(frozen=True, slots=True) -class FormValidationResult(Generic[ValidatedDataT]): +class FormValidationResult[ValidatedDataT]: + """Outcome of validating HTML form input.""" + data: ValidatedDataT | None errors: FormValidationErrors @property def is_valid(self) -> bool: + """Return whether validation succeeded without errors.""" return self.data is not None and not self.errors @dataclass(frozen=True, slots=True) class VenClientFormValues: + """Raw string values submitted by the VEN client create/update form.""" + name: str = "" vtn_url: str = "" oauth_client_id: str = "" @@ -51,20 +63,22 @@ class VenClientFormValues: @classmethod def empty(cls) -> VenClientFormValues: + """Return an empty form state for a new VEN client.""" return cls() def with_field(self, field_name: str, value: str) -> VenClientFormValues: + """Return a copy with one field replaced.""" return replace(self, **{field_name: value}) - def with_request_fields( - self, field_names: tuple[str, ...], form: Mapping[str, str] - ) -> VenClientFormValues: + def with_request_fields(self, field_names: tuple[str, ...], form: Mapping[str, str]) -> VenClientFormValues: + """Return a copy with fields populated from a request form mapping.""" updated = self for field_name in field_names: updated = updated.with_field(field_name, form.get(field_name, "").strip()) return updated def to_validation_input(self) -> dict[str, str]: + """Convert form values to the dict expected by the validation model.""" return { "name": self.name, "vtn_url": self.vtn_url, @@ -77,6 +91,8 @@ def to_validation_input(self) -> dict[str, str]: @dataclass(frozen=True, slots=True) class VenSensorConfigFormValues: + """Raw string values submitted by the polling schedule form.""" + name: str = "" targets: str = "" utc_trigger_time: str = "" @@ -85,27 +101,25 @@ class VenSensorConfigFormValues: @classmethod def empty(cls) -> VenSensorConfigFormValues: + """Return an empty form state for a new polling schedule.""" return cls() @classmethod - def from_post_values( - cls, post_values: VenSensorConfigPostValues - ) -> VenSensorConfigFormValues: + def from_post_values(cls, post_values: VenSensorConfigPostValues) -> VenSensorConfigFormValues: + """Build display form values from validated POST data.""" return cls( name=post_values.name, targets=post_values.targets, utc_trigger_time=post_values.utc_trigger_time, - fetch_import_capacity_limits="on" - if post_values.fetch_import_capacity_limits - else "", - fetch_export_capacity_limits="on" - if post_values.fetch_export_capacity_limits - else "", + fetch_import_capacity_limits="on" if post_values.fetch_import_capacity_limits else "", + fetch_export_capacity_limits="on" if post_values.fetch_export_capacity_limits else "", ) @dataclass(frozen=True, slots=True) class VenSensorConfigPostValues: + """Parsed POST values for a polling schedule form submission.""" + name: str targets: str utc_trigger_time: str @@ -113,6 +127,7 @@ class VenSensorConfigPostValues: fetch_export_capacity_limits: bool def to_validation_input(self) -> dict[str, str | bool]: + """Convert POST values to the dict expected by the validation model.""" return { "name": self.name, "targets": self.targets, diff --git a/flexmeasures_openadr3/models/jobs.py b/flexmeasures_openadr3/models/jobs.py index 161ec5c..e2b68ef 100644 --- a/flexmeasures_openadr3/models/jobs.py +++ b/flexmeasures_openadr3/models/jobs.py @@ -1,11 +1,16 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from datetime import datetime @dataclass(frozen=True, slots=True) class EventActivePeriod: + """Time window during which an OpenADR event interval is active.""" + start: datetime end: datetime event_id: str diff --git a/flexmeasures_openadr3/models/storage.py b/flexmeasures_openadr3/models/storage.py index 0600e56..dd7a0c1 100644 --- a/flexmeasures_openadr3/models/storage.py +++ b/flexmeasures_openadr3/models/storage.py @@ -19,28 +19,23 @@ class VenSensorConfigRecord: @classmethod def from_json(cls, data: Mapping[str, object]) -> VenSensorConfigRecord: + """Build a record from JSON stored on a VEN asset.""" raw_targets = data.get("targets", []) targets: tuple[str, ...] - if isinstance(raw_targets, list): - targets = tuple(str(item) for item in raw_targets) - else: - targets = () + targets = tuple(str(item) for item in raw_targets) if isinstance(raw_targets, list) else () job_id = data.get("fetch_events_job_id") return cls( name=str(data.get("name", "")), targets=targets, utc_trigger_time=str(data.get("utc_trigger_time", "") or ""), - fetch_import_capacity_limits=bool( - data.get("fetch_import_capacity_limits", False) - ), - fetch_export_capacity_limits=bool( - data.get("fetch_export_capacity_limits", False) - ), + fetch_import_capacity_limits=bool(data.get("fetch_import_capacity_limits", False)), + fetch_export_capacity_limits=bool(data.get("fetch_export_capacity_limits", False)), fetch_events_job_id=str(job_id) if job_id is not None else None, ) def to_json(self) -> dict[str, object]: + """Serialize the record for storage in asset attributes.""" payload: dict[str, object] = { "name": self.name, "targets": list(self.targets), @@ -66,12 +61,12 @@ class VenClientAttributePayload: @classmethod def empty(cls) -> VenClientAttributePayload: + """Return an empty VEN attribute payload.""" return cls() @classmethod - def from_asset_attributes( - cls, attributes: Mapping[str, object] | None - ) -> VenClientAttributePayload: + def from_asset_attributes(cls, attributes: Mapping[str, object] | None) -> VenClientAttributePayload: + """Parse VEN settings from a generic asset's attributes column.""" if attributes is None: return cls.empty() @@ -81,19 +76,12 @@ def from_asset_attributes( raw_scopes = raw_payload.get("scopes", []) scopes: tuple[str, ...] - if isinstance(raw_scopes, list): - scopes = tuple(str(item) for item in raw_scopes) - else: - scopes = () + scopes = tuple(str(item) for item in raw_scopes) if isinstance(raw_scopes, list) else () raw_configs = raw_payload.get("sensor_configs", []) sensor_configs: tuple[VenSensorConfigRecord, ...] = () if isinstance(raw_configs, list): - sensor_configs = tuple( - VenSensorConfigRecord.from_json(item) - for item in raw_configs - if isinstance(item, Mapping) - ) + sensor_configs = tuple(VenSensorConfigRecord.from_json(item) for item in raw_configs if isinstance(item, Mapping)) return cls( vtn_url=str(raw_payload.get("vtn_url", "") or ""), @@ -105,6 +93,7 @@ def from_asset_attributes( ) def to_json(self) -> dict[str, object]: + """Serialize the payload for storage in asset attributes.""" return { "vtn_url": self.vtn_url, "oauth_client_id": self.oauth_client_id, diff --git a/flexmeasures_openadr3/py.typed b/flexmeasures_openadr3/py.typed index b648ac9..e69de29 100644 --- a/flexmeasures_openadr3/py.typed +++ b/flexmeasures_openadr3/py.typed @@ -1 +0,0 @@ -partial diff --git a/flexmeasures_openadr3/ui/views/dashboard.py b/flexmeasures_openadr3/ui/views/dashboard.py index 28f380d..3014618 100644 --- a/flexmeasures_openadr3/ui/views/dashboard.py +++ b/flexmeasures_openadr3/ui/views/dashboard.py @@ -1,31 +1,39 @@ +import contextlib from typing import cast from flask import abort, flash, redirect, request, url_for -from werkzeug.wrappers import Response from flask_login import login_required - from flexmeasures.data import db from flexmeasures.ui.utils.view_utils import render_flexmeasures_template +from werkzeug.wrappers import Response -from ... import flexmeasures_openadr3_ui_bp -from ...models.forms import ( - FormValidationErrors, - VenSensorConfigFormValues, - VenSensorConfigPostValues, -) -from ...models.views import VenSensorConfigOverview -from ...utils.encryption import SecretsDecryptionError, SecretsEncryptor -from ...utils.ven_clients import ( +from flexmeasures_openadr3 import flexmeasures_openadr3_ui_bp +from flexmeasures_openadr3.models.forms import FormValidationErrors, VenSensorConfigFormValues, VenSensorConfigPostValues +from flexmeasures_openadr3.models.views import VenSensorConfigOverview +from flexmeasures_openadr3.utils.encryption import SecretsDecryptionError, SecretsEncryptor +from flexmeasures_openadr3.utils.events import ACTIVE_OPENADR_EVENTS, SUPPORTED_SIGNAL_NAMES +from flexmeasures_openadr3.utils.ven_clients import ( + VEN_CLIENT_FORM_FIELDS, VenClient, VenClientRepository, - VEN_CLIENT_FORM_FIELDS, build_ven_client_form_values, build_ven_sensor_config_form_values, validate_ven_client_form, validate_ven_sensor_config_form, ) -from ...utils.events import ACTIVE_OPENADR_EVENTS, SUPPORTED_SIGNAL_NAMES -from ...utils.ven_jobs import VenFetchJobScheduler +from flexmeasures_openadr3.utils.ven_jobs import VenFetchJobScheduler + +_HOUR_MINUTE_COMPONENT_COUNT = 2 +_MAX_HOUR = 23 +_MAX_MINUTE = 59 + + +def _require_validated_data[T](data: T | None) -> T: + """Return validated form data or raise if it is unexpectedly missing.""" + if data is None: + msg = "Validated form data missing despite successful validation." + raise RuntimeError(msg) + return data def _build_repository() -> VenClientRepository: @@ -39,7 +47,8 @@ def _build_job_scheduler(repository: VenClientRepository) -> VenFetchJobSchedule def _utc_trigger_time_from_request(raw: str) -> str: - """Normalize HTML time input values to HH:MM:SS for validation. + """ + Normalize HTML time input values to HH:MM:SS for validation. Browsers may submit HH:MM when seconds are zero; the form validator expects three colon-separated parts. @@ -48,9 +57,9 @@ def _utc_trigger_time_from_request(raw: str) -> str: if not stripped: return "" parts = stripped.split(":") - if len(parts) == 2 and all(p.isdigit() for p in parts): + if len(parts) == _HOUR_MINUTE_COMPONENT_COUNT and all(p.isdigit() for p in parts): hour, minute = int(parts[0]), int(parts[1]) - if 0 <= hour <= 23 and 0 <= minute <= 59: + if 0 <= hour <= _MAX_HOUR and 0 <= minute <= _MAX_MINUTE: return f"{hour:02d}:{minute:02d}:00" return stripped @@ -59,34 +68,23 @@ def _sensor_config_post_values_from_request() -> VenSensorConfigPostValues: return VenSensorConfigPostValues( name=request.form.get("name", "").strip(), targets=request.form.get("targets", "").strip(), - utc_trigger_time=_utc_trigger_time_from_request( - request.form.get("utc_trigger_time", "") - ), - fetch_import_capacity_limits=request.form.get("fetch_import_capacity_limits") - == "on", - fetch_export_capacity_limits=request.form.get("fetch_export_capacity_limits") - == "on", + utc_trigger_time=_utc_trigger_time_from_request(request.form.get("utc_trigger_time", "")), + fetch_import_capacity_limits=request.form.get("fetch_import_capacity_limits") == "on", + fetch_export_capacity_limits=request.form.get("fetch_export_capacity_limits") == "on", ) def _decrypt_ven_client_credentials_for_form(ven_client: VenClient) -> VenClient: - """Decrypt VEN credentials for UI rendering. + """ + Decrypt VEN credentials for UI rendering. If credentials are already plaintext (legacy records), keep them unchanged. """ secrets_encryptor = SecretsEncryptor.from_current_app() - try: - ven_client.oauth_client_id = secrets_encryptor.decrypt( - ven_client.oauth_client_id - ) - except SecretsDecryptionError: - pass - try: - ven_client.oauth_client_secret = secrets_encryptor.decrypt( - ven_client.oauth_client_secret - ) - except SecretsDecryptionError: - pass + with contextlib.suppress(SecretsDecryptionError): + ven_client.oauth_client_id = secrets_encryptor.decrypt(ven_client.oauth_client_id) + with contextlib.suppress(SecretsDecryptionError): + ven_client.oauth_client_secret = secrets_encryptor.decrypt(ven_client.oauth_client_secret) return ven_client @@ -96,17 +94,10 @@ def _decrypt_ven_client_credentials_for_form(ven_client: VenClient) -> VenClient def dashboard() -> str: """Render the OpenADR 3 dashboard.""" ven_client_repository = _build_repository() - active_events = tuple( - event - for event in ACTIVE_OPENADR_EVENTS - if all( - pd.payload_type in SUPPORTED_SIGNAL_NAMES - for pd in event.payload_descriptors or () - ) - ) + active_events = tuple(event for event in ACTIVE_OPENADR_EVENTS if all(pd.payload_type in SUPPORTED_SIGNAL_NAMES for pd in event.payload_descriptors or ())) return cast( - str, + "str", render_flexmeasures_template( "flexmeasures_oadr3_dashboard.html", ven_clients=ven_client_repository.list_ven_clients(), @@ -140,7 +131,7 @@ def sensor_configs_overview(ven_id: int) -> str: ] return cast( - str, + "str", render_flexmeasures_template( "flexmeasures_oadr3_sensor_configs_overview.html", ven_client=ven_client, @@ -158,9 +149,7 @@ def ven_client_new() -> str | Response: errors = FormValidationErrors() if request.method == "POST": - field_values = field_values.with_request_fields( - VEN_CLIENT_FORM_FIELDS, request.form - ) + field_values = field_values.with_request_fields(VEN_CLIENT_FORM_FIELDS, request.form) validation = validate_ven_client_form( field_values=field_values, ven_client_repository=ven_client_repository, @@ -168,14 +157,13 @@ def ven_client_new() -> str | Response: errors = validation.errors if validation.is_valid: - assert validation.data is not None - ven_client = ven_client_repository.create(validation.data) + ven_client = ven_client_repository.create(_require_validated_data(validation.data)) db.session.commit() flash(f"VEN client '{ven_client.name}' created.") return redirect(url_for(".dashboard")) return cast( - str, + "str", render_flexmeasures_template( "flexmeasures_oadr3_ven_client_new.html", form_values=field_values, @@ -195,7 +183,7 @@ def ven_client_detail(ven_id: int) -> str: ven_client = _decrypt_ven_client_credentials_for_form(ven_client) return cast( - str, + "str", render_flexmeasures_template( "flexmeasures_oadr3_ven_client_detail.html", ven_client=ven_client, @@ -214,9 +202,7 @@ def ven_client_update(ven_id: int) -> str | Response: if ven_client is None: abort(404) - field_values = build_ven_client_form_values(ven_client).with_request_fields( - VEN_CLIENT_FORM_FIELDS, request.form - ) + field_values = build_ven_client_form_values(ven_client).with_request_fields(VEN_CLIENT_FORM_FIELDS, request.form) validation = validate_ven_client_form( field_values, ven_client_repository=ven_client_repository, @@ -224,7 +210,7 @@ def ven_client_update(ven_id: int) -> str | Response: ) if not validation.is_valid: return cast( - str, + "str", render_flexmeasures_template( "flexmeasures_oadr3_ven_client_detail.html", ven_client=ven_client, @@ -233,8 +219,7 @@ def ven_client_update(ven_id: int) -> str | Response: ), ) - assert validation.data is not None - ven_client = ven_client_repository.update(ven_client, validation.data) + ven_client = ven_client_repository.update(ven_client, _require_validated_data(validation.data)) db.session.commit() flash(f"VEN client '{ven_client.name}' updated.") return redirect(url_for(".ven_client_detail", ven_id=ven_client.id)) @@ -257,9 +242,7 @@ def ven_client_delete(ven_id: int) -> Response: return redirect(url_for(".dashboard")) -@flexmeasures_openadr3_ui_bp.route( - "/ven-clients//sensor-configs/new", methods=["GET"] -) +@flexmeasures_openadr3_ui_bp.route("/ven-clients//sensor-configs/new", methods=["GET"]) @login_required def ven_client_sensor_config_new(ven_id: int) -> str: """Render the 'new polling schedule' form for a VEN client.""" @@ -269,23 +252,19 @@ def ven_client_sensor_config_new(ven_id: int) -> str: abort(404) return cast( - str, + "str", render_flexmeasures_template( "flexmeasures_oadr3_ven_sensor_config_form.html", ven_client=ven_client, form_values=build_ven_sensor_config_form_values(ven_client), form_errors=FormValidationErrors(), - form_action=url_for( - ".ven_client_sensor_config_create", ven_id=ven_client.id - ), + form_action=url_for(".ven_client_sensor_config_create", ven_id=ven_client.id), title="Add polling schedule", ), ) -@flexmeasures_openadr3_ui_bp.route( - "/ven-clients//sensor-configs", methods=["POST"] -) +@flexmeasures_openadr3_ui_bp.route("/ven-clients//sensor-configs", methods=["POST"]) @login_required def ven_client_sensor_config_create(ven_id: int) -> str | Response: """Handle form submission for creating a polling schedule.""" @@ -296,13 +275,11 @@ def ven_client_sensor_config_create(ven_id: int) -> str | Response: abort(404) post_values = _sensor_config_post_values_from_request() - validation = validate_ven_sensor_config_form( - post_values, existing_configs=ven_client.sensor_configs - ) + validation = validate_ven_sensor_config_form(post_values, existing_configs=ven_client.sensor_configs) if validation.is_valid: - assert validation.data is not None sensor_config = ven_client_repository.append_sensor_config( - ven_client, validation.data + ven_client, + _require_validated_data(validation.data), ) job_scheduler.schedule(ven_client, sensor_config) db.session.commit() @@ -310,15 +287,13 @@ def ven_client_sensor_config_create(ven_id: int) -> str | Response: return redirect(url_for(".dashboard")) return cast( - str, + "str", render_flexmeasures_template( "flexmeasures_oadr3_ven_sensor_config_form.html", ven_client=ven_client, form_values=VenSensorConfigFormValues.from_post_values(post_values), form_errors=validation.errors, - form_action=url_for( - ".ven_client_sensor_config_create", ven_id=ven_client.id - ), + form_action=url_for(".ven_client_sensor_config_create", ven_id=ven_client.id), title="Add polling schedule", ), ) @@ -364,7 +339,7 @@ def ven_client_sensor_config_edit(ven_id: int, config_name: str) -> str: abort(404) return cast( - str, + "str", render_flexmeasures_template( "flexmeasures_oadr3_ven_sensor_config_form.html", ven_client=ven_client, @@ -408,12 +383,14 @@ def ven_client_sensor_config_update(ven_id: int, config_name: str) -> str | Resp current_name=config_name, ) if validation.is_valid: - assert validation.data is not None existing_config = ven_client.get_sensor_config(config_name) - assert existing_config is not None + if existing_config is None: + abort(404) job_scheduler.delete(existing_config) updated_config = ven_client_repository.update_sensor_config( - ven_client, config_name, validation.data + ven_client, + config_name, + _require_validated_data(validation.data), ) job_scheduler.schedule(ven_client, updated_config) db.session.commit() @@ -421,7 +398,7 @@ def ven_client_sensor_config_update(ven_id: int, config_name: str) -> str | Resp return redirect(url_for(".dashboard")) return cast( - str, + "str", render_flexmeasures_template( "flexmeasures_oadr3_ven_sensor_config_form.html", ven_client=ven_client, diff --git a/flexmeasures_openadr3/utils/blueprints.py b/flexmeasures_openadr3/utils/blueprints.py index 0dd8ebb..7610484 100644 --- a/flexmeasures_openadr3/utils/blueprints.py +++ b/flexmeasures_openadr3/utils/blueprints.py @@ -1,10 +1,11 @@ -import sys import importlib +import sys def ensure_bp_routes_are_loaded_fresh(module_name: str) -> None: """ Reload a module if it has been loaded before. + It's useful for situations in which some other process has read the module before, but you need some action to happen which only happens during module import ― decorators are a good example. diff --git a/flexmeasures_openadr3/utils/encryption.py b/flexmeasures_openadr3/utils/encryption.py index eb37e2f..47c8ab0 100644 --- a/flexmeasures_openadr3/utils/encryption.py +++ b/flexmeasures_openadr3/utils/encryption.py @@ -1,18 +1,17 @@ -from flask import current_app - import base64 from dataclasses import dataclass from cryptography.fernet import Fernet, InvalidToken from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from flask import current_app class SecretsEncryptionError(Exception): """Raised when secrets encryption/decryption fails.""" -class InvalidSecretsEncryptionKey(SecretsEncryptionError): +class InvalidSecretsEncryptionKeyError(SecretsEncryptionError): """Raised when a usable master key cannot be loaded.""" @@ -40,16 +39,17 @@ def _get_secrets_encryption_key() -> str: @classmethod def from_current_app(cls) -> "SecretsEncryptor": + """Build an encryptor using keys from the current Flask app config.""" encryption_key = cls._get_secrets_encryption_key() if not isinstance(encryption_key, str) or not encryption_key.strip(): - raise InvalidSecretsEncryptionKey( - "Missing OPENADR_SECRETS_ENCRYPTION_KEY or SECRET_KEY." - ) + msg = "Missing OPENADR_SECRETS_ENCRYPTION_KEY or SECRET_KEY." + raise InvalidSecretsEncryptionKeyError(msg) return cls(_encryption_key=encryption_key) def _fernet_for(self, secret_key: str) -> Fernet: if not isinstance(secret_key, str) or not secret_key: - raise InvalidSecretsEncryptionKey("secret_key must be a non-empty string.") + msg = "secret_key must be a non-empty string." + raise InvalidSecretsEncryptionKeyError(msg) hkdf = HKDF( algorithm=hashes.SHA256(), @@ -63,7 +63,8 @@ def _fernet_for(self, secret_key: str) -> Fernet: def encrypt(self, value: str) -> str: """Encrypt a string and return a URL-safe token.""" if not isinstance(value, str): - raise SecretsEncryptionError("value must be a string.") + msg = "value must be a string." + raise SecretsEncryptionError(msg) token = self._fernet_for(self._encryption_key).encrypt(value.encode("utf-8")) return token.decode("utf-8") @@ -71,12 +72,12 @@ def encrypt(self, value: str) -> str: def decrypt(self, token: str) -> str: """Decrypt a token and return the original string.""" if not isinstance(token, str): - raise SecretsDecryptionError("token must be a string.") + msg = "token must be a string." + raise SecretsDecryptionError(msg) try: raw = self._fernet_for(self._encryption_key).decrypt(token.encode("utf-8")) except InvalidToken as exc: - raise SecretsDecryptionError( - "Invalid token for the given encryption_key." - ) from exc + msg = "Invalid token for the given encryption_key." + raise SecretsDecryptionError(msg) from exc return raw.decode("utf-8") diff --git a/flexmeasures_openadr3/utils/events.py b/flexmeasures_openadr3/utils/events.py index 15f1b5a..ccd091b 100644 --- a/flexmeasures_openadr3/utils/events.py +++ b/flexmeasures_openadr3/utils/events.py @@ -1,13 +1,13 @@ -from datetime import datetime, timedelta, timezone import uuid +from datetime import UTC, datetime, timedelta +from openadr3_client._models.common.interval_period import IntervalPeriod +from openadr3_client.oadr310.models.event.event import ExistingEvent, Interval from openadr3_client.oadr310.models.event.event_payload import ( EventPayload, - EventPayloadType, EventPayloadDescriptor, + EventPayloadType, ) -from openadr3_client.oadr310.models.event.event import ExistingEvent, Interval -from openadr3_client._models.common.interval_period import IntervalPeriod from openadr3_client.oadr310.models.unit import Unit SUPPORTED_SIGNAL_NAMES: tuple[EventPayloadType, ...] = ( @@ -20,8 +20,8 @@ id=str(uuid.uuid4()), programID="program-1", event_name="event-1", - created_date_time=datetime.now(tz=timezone.utc), - modification_date_time=datetime.now(tz=timezone.utc), + created_date_time=datetime.now(tz=UTC), + modification_date_time=datetime.now(tz=UTC), priority=1, targets=("site:main", "market_role:prosumer"), payload_descriptors=( @@ -34,14 +34,10 @@ Interval( id=1, interval_period=IntervalPeriod( - start=datetime.now(tz=timezone.utc), + start=datetime.now(tz=UTC), duration=timedelta(hours=1), ), - payloads=( - EventPayload( - type=EventPayloadType.IMPORT_CAPACITY_AVAILABLE, values=(200,) - ), - ), + payloads=(EventPayload(type=EventPayloadType.IMPORT_CAPACITY_AVAILABLE, values=(200,)),), ), ), ), diff --git a/flexmeasures_openadr3/utils/sensor.py b/flexmeasures_openadr3/utils/sensor.py index 62477c0..9084005 100644 --- a/flexmeasures_openadr3/utils/sensor.py +++ b/flexmeasures_openadr3/utils/sensor.py @@ -2,23 +2,22 @@ from dataclasses import dataclass from datetime import timedelta -from typing import TYPE_CHECKING, Generic, TypeAlias, TypeVar, cast +from typing import TYPE_CHECKING, TypeVar, cast -from flask_login import current_user -from sqlalchemy import select, delete import sqlalchemy as sa - +from flask_login import current_user from flexmeasures.data import db +from flexmeasures.data.models.audit_log import AssetAuditLog from flexmeasures.data.models.generic_assets import GenericAsset, GenericAssetType from flexmeasures.data.models.time_series import Sensor, TimedBelief -from flexmeasures.data.models.audit_log import AssetAuditLog from flexmeasures.utils.time_utils import get_timezone +from sqlalchemy import delete, select if TYPE_CHECKING: from flexmeasures_openadr3.utils.ven_clients import VenSensorConfig -JsonScalar: TypeAlias = int | str | None -JsonValue: TypeAlias = JsonScalar | dict[str, "JsonValue"] | list["JsonValue"] +type JsonScalar = int | str | None +type JsonValue = JsonScalar | dict[str, "JsonValue"] | list["JsonValue"] PrunedValueT = TypeVar("PrunedValueT", bound=JsonValue | None) @@ -28,6 +27,7 @@ class RemoveMarker: __slots__ = () def __repr__(self) -> str: + """Return a stable debug representation.""" return "REMOVE" @@ -35,12 +35,15 @@ def __repr__(self) -> str: @dataclass(frozen=True, slots=True) -class PruneResult(Generic[PrunedValueT]): +class PruneResult[PrunedValueT: JsonValue | None]: + """Result of pruning sensor references from a JSON subtree.""" + value: PrunedValueT | RemoveMarker changed: bool @property def should_remove(self) -> bool: + """Return whether the pruned value should be removed from its parent.""" return self.value is REMOVE @@ -53,10 +56,9 @@ def should_remove(self) -> bool: VEN_SENSOR_EVENT_RESOLUTION = timedelta(minutes=15) -def _prune_flex_config_sensor_refs( - value: JsonValue, sensor_id: int -) -> PruneResult[JsonValue]: - """Recursively remove sensor references from nested flex_model/flex_context JSON structures. +def _prune_flex_config_sensor_refs(value: JsonValue, sensor_id: int) -> PruneResult[JsonValue]: # noqa: C901 + """ + Recursively remove sensor references from nested flex_model/flex_context JSON structures. This function handles deeply nested JSON objects and lists from flex_model and flex_context JSONB columns. It scans for sensor references in two forms: @@ -82,6 +84,7 @@ def _prune_flex_config_sensor_refs( {'limit': '10 kW'} >>> did_change True + """ if isinstance(value, dict): if set(value.keys()) == {"sensor"} and value.get("sensor") == sensor_id: @@ -103,7 +106,7 @@ def _prune_flex_config_sensor_refs( changed = True continue if nested_result.value is not REMOVE: - pruned_dict[key] = cast(JsonValue, nested_result.value) + pruned_dict[key] = cast("JsonValue", nested_result.value) return PruneResult(value=pruned_dict, changed=changed) if isinstance(value, list): @@ -116,16 +119,15 @@ def _prune_flex_config_sensor_refs( changed = True continue if item_result.value is not REMOVE: - pruned_list.append(cast(JsonValue, item_result.value)) + pruned_list.append(cast("JsonValue", item_result.value)) return PruneResult(value=pruned_list, changed=changed) return PruneResult(value=value, changed=False) -def _prune_sensors_to_show_refs( - value: list[JsonValue] | None, sensor_id: int -) -> PruneResult[list[JsonValue] | None]: - """Remove sensor references from sensors_to_show JSON list. +def _prune_sensors_to_show_refs(value: list[JsonValue] | None, sensor_id: int) -> PruneResult[list[JsonValue] | None]: # noqa: C901 + """ + Remove sensor references from sensors_to_show JSON list. This function handles sensors_to_show lists which support multiple entry formats: - Bare sensor IDs: [42, 43, ...] @@ -149,6 +151,7 @@ def _prune_sensors_to_show_refs( [[43]] >>> did_change True + """ if not isinstance(value, list): return PruneResult(value=value, changed=False) @@ -180,7 +183,7 @@ def _prune_sensors_to_show_refs( if entry_result.should_remove: continue if entry_result.value is not REMOVE: - cleaned.append(cast(JsonValue, entry_result.value)) + cleaned.append(cast("JsonValue", entry_result.value)) continue cleaned.append(entry) @@ -188,10 +191,9 @@ def _prune_sensors_to_show_refs( return PruneResult(value=cleaned, changed=changed) -def _prune_sensors_to_show_entry( - entry: dict[str, JsonValue], sensor_id: int -) -> PruneResult[dict[str, JsonValue]]: - """Remove sensor references from a single sensors_to_show dict entry. +def _prune_sensors_to_show_entry(entry: dict[str, JsonValue], sensor_id: int) -> PruneResult[dict[str, JsonValue]]: # noqa: C901 + """ + Remove sensor references from a single sensors_to_show dict entry. Handles three field types within a dict entry: - "sensor": Direct sensor ID reference → remove if matches @@ -214,6 +216,7 @@ def _prune_sensors_to_show_entry( >>> pruned, did_change = _prune_sensors_to_show_entry(entry, sensor_id=42) >>> pruned is _REMOVE True + """ if "sensor" in entry: if entry.get("sensor") == sensor_id: @@ -261,10 +264,9 @@ def _prune_sensors_to_show_entry( return PruneResult(value=entry, changed=False) -def _prune_sensors_to_show_as_kpis_refs( - value: list[JsonValue] | None, sensor_id: int -) -> PruneResult[list[JsonValue] | None]: - """Remove sensor references from sensors_to_show_as_kpis JSON list. +def _prune_sensors_to_show_as_kpis_refs(value: list[JsonValue] | None, sensor_id: int) -> PruneResult[list[JsonValue] | None]: + """ + Remove sensor references from sensors_to_show_as_kpis JSON list. This function handles sensors_to_show_as_kpis lists which support: - Bare sensor IDs: [42, 43, ...] @@ -287,6 +289,7 @@ def _prune_sensors_to_show_as_kpis_refs( [] >>> did_change True + """ if not isinstance(value, list): return PruneResult(value=value, changed=False) @@ -307,6 +310,8 @@ def _prune_sensors_to_show_as_kpis_refs( @dataclass(frozen=True, slots=True) class VenSensorPair: + """Import and export sensors linked to a polling schedule.""" + import_sensor: Sensor | None export_sensor: Sensor | None @@ -315,9 +320,8 @@ class VenAssetRepository: """Handles persistence of VEN GenericAsset and GenericAssetType objects.""" def get_or_create_asset_type(self) -> GenericAssetType: - asset_type = db.session.execute( - select(GenericAssetType).filter_by(name=VEN_ASSET_TYPE_NAME) - ).scalar_one_or_none() + """Return the OpenADR VEN asset type, creating it if needed.""" + asset_type = db.session.execute(select(GenericAssetType).filter_by(name=VEN_ASSET_TYPE_NAME)).scalar_one_or_none() if asset_type is None: asset_type = GenericAssetType( @@ -330,6 +334,7 @@ def get_or_create_asset_type(self) -> GenericAssetType: return asset_type def create_asset(self, asset_name: str) -> GenericAsset: + """Create a new VEN generic asset for the current user.""" asset_type = self.get_or_create_asset_type() existing = db.session.execute( select(GenericAsset).filter_by( @@ -339,7 +344,8 @@ def create_asset(self, asset_name: str) -> GenericAsset: ).scalar_one_or_none() if existing is not None: - raise ValueError(f"Asset with name '{asset_name}' already exists.") + msg = f"Asset with name '{asset_name}' already exists." + raise ValueError(msg) asset = GenericAsset( name=asset_name, @@ -351,22 +357,17 @@ def create_asset(self, asset_name: str) -> GenericAsset: return asset def find_by_id(self, asset_id: int) -> GenericAsset | None: - return db.session.execute( - select(GenericAsset).filter_by(id=asset_id) - ).scalar_one_or_none() + """Find a VEN asset by database id.""" + return db.session.execute(select(GenericAsset).filter_by(id=asset_id)).scalar_one_or_none() def find_by_name(self, asset_name: str) -> GenericAsset | None: - return db.session.execute( - select(GenericAsset).filter_by(name=asset_name) - ).scalar_one_or_none() + """Find a VEN asset by name.""" + return db.session.execute(select(GenericAsset).filter_by(name=asset_name)).scalar_one_or_none() def list_all(self) -> list[GenericAsset]: + """List all generic assets of the OpenADR VEN type.""" asset_type = self.get_or_create_asset_type() - return list( - db.session.execute( - select(GenericAsset).filter_by(generic_asset_type_id=asset_type.id) - ).scalars() - ) + return list(db.session.execute(select(GenericAsset).filter_by(generic_asset_type_id=asset_type.id)).scalars()) class VenSensorRepository: @@ -377,11 +378,11 @@ def _cleanup_sensor_references_in_assets( sensor_id: int, sensor_name: str | None = None, ) -> int: - """Remove references to a sensor in JSONB config fields across assets. + """ + Remove references to a sensor in JSONB config fields across assets. Returns the number of updated assets. """ - vars_json = sa.func.jsonb_build_object("sid", sensor_id) candidates = db.session.scalars( sa.select(GenericAsset).where( @@ -427,18 +428,10 @@ def _cleanup_sensor_references_in_assets( changed_assets = 0 for asset in candidates: - flex_model_result = _prune_flex_config_sensor_refs( - asset.flex_model, sensor_id - ) - flex_context_result = _prune_flex_config_sensor_refs( - asset.flex_context, sensor_id - ) - sensors_to_show_result = _prune_sensors_to_show_refs( - asset.sensors_to_show, sensor_id - ) - sensors_to_show_as_kpis_result = _prune_sensors_to_show_as_kpis_refs( - asset.sensors_to_show_as_kpis, sensor_id - ) + flex_model_result = _prune_flex_config_sensor_refs(asset.flex_model, sensor_id) + flex_context_result = _prune_flex_config_sensor_refs(asset.flex_context, sensor_id) + sensors_to_show_result = _prune_sensors_to_show_refs(asset.sensors_to_show, sensor_id) + sensors_to_show_as_kpis_result = _prune_sensors_to_show_as_kpis_refs(asset.sensors_to_show_as_kpis, sensor_id) changed = any( ( @@ -460,9 +453,7 @@ def _cleanup_sensor_references_in_assets( for field_changed, field_name in changed_field_events: if not field_changed: continue - sensor_label = ( - f"'{sensor_name}': {sensor_id}" if sensor_name else str(sensor_id) - ) + sensor_label = f"'{sensor_name}': {sensor_id}" if sensor_name else str(sensor_id) AssetAuditLog.add_record( asset, f"Removed sensor reference {sensor_label} from {field_name} (because sensor has been deleted).", @@ -482,7 +473,8 @@ def _cleanup_sensor_references_in_assets( return changed_assets def _delete_sensor(self, sensor: Sensor) -> None: - """Delete a sensor and all its time series data. + """ + Delete a sensor and all its time series data. Does not commit the session. Cleans up sensor references in asset JSONB fields. @@ -491,12 +483,11 @@ def _delete_sensor(self, sensor: Sensor) -> None: sensor_name = sensor.name self._cleanup_sensor_references_in_assets(sensor.id, sensor.name) db.session.execute(delete(TimedBelief).filter_by(sensor_id=sensor.id)) - AssetAuditLog.add_record( - sensor.generic_asset, f"Deleted sensor '{sensor_name}': {sensor.id}" - ) + AssetAuditLog.add_record(sensor.generic_asset, f"Deleted sensor '{sensor_name}': {sensor.id}") db.session.execute(delete(Sensor).filter_by(id=sensor.id)) def find_sensor(self, sensor_name: str, asset: GenericAsset) -> Sensor | None: + """Find a sensor by name on the given VEN asset.""" return db.session.execute( select(Sensor).filter_by( name=sensor_name, @@ -505,6 +496,7 @@ def find_sensor(self, sensor_name: str, asset: GenericAsset) -> Sensor | None: ).scalar_one_or_none() def get_or_create_sensor(self, sensor_name: str, asset: GenericAsset) -> Sensor: + """Return an existing sensor or create one with default VEN settings.""" sensor = self.find_sensor(sensor_name, asset) if sensor is None: @@ -522,44 +514,34 @@ def get_or_create_sensor(self, sensor_name: str, asset: GenericAsset) -> Sensor: return sensor def import_sensor_name(self, config_name: str) -> str: + """Return the FlexMeasures sensor name for import capacity limits.""" return f"{IMPORT_CAPACITY_LIMIT_SENSOR_NAME}-{config_name}" def export_sensor_name(self, config_name: str) -> str: + """Return the FlexMeasures sensor name for export capacity limits.""" return f"{EXPORT_CAPACITY_LIMIT_SENSOR_NAME}-{config_name}" - def resolve_sensors_for_config( - self, asset: GenericAsset, sensor_config: VenSensorConfig - ) -> VenSensorPair: + def resolve_sensors_for_config(self, asset: GenericAsset, sensor_config: VenSensorConfig) -> VenSensorPair: """Find existing sensors for a config without creating them.""" import_sensor = None export_sensor = None if sensor_config.fetch_import_capacity_limits: - import_sensor = self.find_sensor( - self.import_sensor_name(sensor_config.name), asset - ) + import_sensor = self.find_sensor(self.import_sensor_name(sensor_config.name), asset) if sensor_config.fetch_export_capacity_limits: - export_sensor = self.find_sensor( - self.export_sensor_name(sensor_config.name), asset - ) + export_sensor = self.find_sensor(self.export_sensor_name(sensor_config.name), asset) return VenSensorPair(import_sensor=import_sensor, export_sensor=export_sensor) - def ensure_sensors_for_config( - self, asset: GenericAsset, sensor_config: VenSensorConfig - ) -> VenSensorPair: + def ensure_sensors_for_config(self, asset: GenericAsset, sensor_config: VenSensorConfig) -> VenSensorPair: """Get or create the sensors required by a config.""" import_sensor = None export_sensor = None if sensor_config.fetch_import_capacity_limits: - import_sensor = self.get_or_create_sensor( - self.import_sensor_name(sensor_config.name), asset - ) + import_sensor = self.get_or_create_sensor(self.import_sensor_name(sensor_config.name), asset) if sensor_config.fetch_export_capacity_limits: - export_sensor = self.get_or_create_sensor( - self.export_sensor_name(sensor_config.name), asset - ) + export_sensor = self.get_or_create_sensor(self.export_sensor_name(sensor_config.name), asset) return VenSensorPair(import_sensor=import_sensor, export_sensor=export_sensor) diff --git a/flexmeasures_openadr3/utils/ven_clients.py b/flexmeasures_openadr3/utils/ven_clients.py index 590bae3..3a0be99 100644 --- a/flexmeasures_openadr3/utils/ven_clients.py +++ b/flexmeasures_openadr3/utils/ven_clients.py @@ -3,16 +3,15 @@ from dataclasses import dataclass, field from flask import current_app +from flexmeasures.data import db +from flexmeasures.data.models.generic_assets import GenericAsset +from flexmeasures.data.models.time_series import Sensor from openadr3_client.oadr310._ven.client import VirtualEndNodeClient from openadr3_client.ven.http_factory import VirtualEndNodeHttpClientFactory from openadr3_client.version import OADRVersion from pydantic import BaseModel, Field, ValidationError, field_validator from pydantic_core.core_schema import ValidationInfo -from flexmeasures.data import db -from flexmeasures.data.models.generic_assets import GenericAsset -from flexmeasures.data.models.time_series import Sensor - from flexmeasures_openadr3.models.forms import ( FormValidationErrors, FormValidationResult, @@ -28,6 +27,11 @@ from flexmeasures_openadr3.utils.encryption import SecretsEncryptor from flexmeasures_openadr3.utils.sensor import VenAssetRepository, VenSensorRepository +_TIME_COMPONENT_COUNT = 3 +_MAX_HOUR = 23 +_MAX_MINUTE = 59 +_MAX_SECOND = 59 + def _parse_csv_values(raw_value: str) -> list[str]: """Parse a comma-separated string into a list of trimmed, non-empty values.""" @@ -41,7 +45,8 @@ def _parse_csv_values(raw_value: str) -> list[str]: @dataclass class VenSensorConfig: - """A polling schedule that defines when and what to fetch from a VTN. + """ + A polling schedule that defines when and what to fetch from a VTN. Each config maps to zero, one, or two FlexMeasures sensors (import/export). """ @@ -56,6 +61,7 @@ class VenSensorConfig: export_sensor: Sensor | None = field(default=None, repr=False) def to_record(self) -> VenSensorConfigRecord: + """Serialize this config to a storable record.""" return VenSensorConfigRecord( name=self.name, targets=tuple(self.targets), @@ -67,6 +73,7 @@ def to_record(self) -> VenSensorConfigRecord: @classmethod def from_record(cls, record: VenSensorConfigRecord) -> VenSensorConfig: + """Build a config from a stored record without linked sensors.""" return cls( name=record.name, targets=list(record.targets), @@ -79,7 +86,8 @@ def from_record(cls, record: VenSensorConfigRecord) -> VenSensorConfig: @dataclass class VenClient: - """Domain model representing an OpenADR VEN client. + """ + Domain model representing an OpenADR VEN client. Wraps a FlexMeasures GenericAsset and provides typed access to VEN-specific attributes stored in the asset's JSON attributes column. @@ -95,27 +103,28 @@ class VenClient: @property def id(self) -> int: + """Return the underlying FlexMeasures asset id.""" return int(self.asset.id) @property def name(self) -> str: + """Return the VEN client display name.""" return str(self.asset.name) @name.setter def name(self, value: str) -> None: + """Update the VEN client display name.""" self.asset.name = value def get_sensor_config(self, config_name: str) -> VenSensorConfig | None: - return next( - (cfg for cfg in self.sensor_configs if cfg.name == config_name), None - ) + """Return a polling schedule by name, if it exists.""" + return next((cfg for cfg in self.sensor_configs if cfg.name == config_name), None) def create_http_client(self) -> VirtualEndNodeClient: + """Build an authenticated OpenADR HTTP client for this VEN.""" secrets_encryptor = SecretsEncryptor.from_current_app() decrypted_oauth_client_id = secrets_encryptor.decrypt(self.oauth_client_id) - decrypted_oauth_client_secret = secrets_encryptor.decrypt( - self.oauth_client_secret - ) + decrypted_oauth_client_secret = secrets_encryptor.decrypt(self.oauth_client_secret) return VirtualEndNodeHttpClientFactory.create_http_ven_client( vtn_base_url=self.vtn_url, @@ -123,14 +132,12 @@ def create_http_client(self) -> VirtualEndNodeClient: client_secret=decrypted_oauth_client_secret, token_url=self.oauth_token_url, scopes=self.scopes, - allow_insecure_http=current_app.config.get( - "ALLOW_INSECURE_HTTP_VTN", "false" - ) - == "true", + allow_insecure_http=current_app.config.get("ALLOW_INSECURE_HTTP_VTN", "false") == "true", version=OADRVersion.OADR_310, ) # type: ignore[return-value] def to_attribute_payload(self) -> VenClientAttributePayload: + """Convert this client to the JSON payload stored on the asset.""" return VenClientAttributePayload( vtn_url=self.vtn_url, oauth_client_id=self.oauth_client_id, @@ -154,9 +161,7 @@ def __init__(self) -> None: self._sensors = VenSensorRepository() self._secrets_encryptor = SecretsEncryptor.from_current_app() - def _build_sensor_configs( - self, asset: GenericAsset, records: tuple[VenSensorConfigRecord, ...] - ) -> list[VenSensorConfig]: + def _build_sensor_configs(self, asset: GenericAsset, records: tuple[VenSensorConfigRecord, ...]) -> list[VenSensorConfig]: configs: list[VenSensorConfig] = [] for record in records: sensor_config = VenSensorConfig.from_record(record) @@ -179,10 +184,9 @@ def _build_ven_client(self, asset: GenericAsset) -> VenClient: sensor_configs=self._build_sensor_configs(asset, payload.sensor_configs), ) - def _persist_payload( - self, ven_client: VenClient, *, encrypt_oauth_credentials: bool = False - ) -> None: - """Write VEN-specific data back to the asset's attributes column. + def _persist_payload(self, ven_client: VenClient, *, encrypt_oauth_credentials: bool = False) -> None: + """ + Write VEN-specific data back to the asset's attributes column. OAuth client id and secret are stored encrypted. Pass *encrypt_oauth_credentials* True when *ven_client* carries plaintext from a form; pass False when those @@ -191,12 +195,8 @@ def _persist_payload( """ attributes = dict(ven_client.asset.attributes or {}) if encrypt_oauth_credentials: - stored_oauth_client_id = self._secrets_encryptor.encrypt( - ven_client.oauth_client_id - ) - stored_oauth_client_secret = self._secrets_encryptor.encrypt( - ven_client.oauth_client_secret - ) + stored_oauth_client_id = self._secrets_encryptor.encrypt(ven_client.oauth_client_id) + stored_oauth_client_secret = self._secrets_encryptor.encrypt(ven_client.oauth_client_secret) else: stored_oauth_client_id = ven_client.oauth_client_id stored_oauth_client_secret = ven_client.oauth_client_secret @@ -213,17 +213,21 @@ def _persist_payload( ven_client.asset.attributes = attributes def list_ven_clients(self) -> list[VenClient]: + """Return all persisted VEN clients.""" return [self._build_ven_client(asset) for asset in self._assets.list_all()] def find_by_id(self, ven_id: int) -> VenClient | None: + """Find a VEN client by asset id.""" asset = self._assets.find_by_id(ven_id) return self._build_ven_client(asset) if asset else None def find_by_name(self, ven_name: str) -> VenClient | None: + """Find a VEN client by asset name.""" asset = self._assets.find_by_name(ven_name) return self._build_ven_client(asset) if asset else None def create(self, form_data: VenClientFormData) -> VenClient: + """Create a new VEN client from validated form data.""" asset = self._assets.create_asset(form_data.name) ven_client = VenClient( asset=asset, @@ -238,6 +242,7 @@ def create(self, form_data: VenClientFormData) -> VenClient: return self._build_ven_client(asset) def update(self, ven_client: VenClient, form_data: VenClientFormData) -> VenClient: + """Update an existing VEN client from validated form data.""" ven_client.name = form_data.name ven_client.vtn_url = form_data.vtn_url ven_client.oauth_client_id = form_data.oauth_client_id @@ -249,16 +254,14 @@ def update(self, ven_client: VenClient, form_data: VenClientFormData) -> VenClie return ven_client def delete(self, ven_client: VenClient) -> None: + """Delete a VEN client and its underlying asset.""" db.session.delete(ven_client.asset) - def append_sensor_config( - self, ven_client: VenClient, form_data: VenSensorConfigFormData - ) -> VenSensorConfig: + def append_sensor_config(self, ven_client: VenClient, form_data: VenSensorConfigFormData) -> VenSensorConfig: """Append a new sensor config and ensure its sensors exist.""" if ven_client.get_sensor_config(form_data.name) is not None: - raise ValueError( - f"Sensor config with name '{form_data.name}' already exists." - ) + msg = f"Sensor config with name '{form_data.name}' already exists." + raise ValueError(msg) sensor_config = VenSensorConfig( name=form_data.name, @@ -268,9 +271,7 @@ def append_sensor_config( fetch_export_capacity_limits=form_data.fetch_export_capacity_limits, ) - sensors = self._sensors.ensure_sensors_for_config( - ven_client.asset, sensor_config - ) + sensors = self._sensors.ensure_sensors_for_config(ven_client.asset, sensor_config) sensor_config.import_sensor = sensors.import_sensor sensor_config.export_sensor = sensors.export_sensor @@ -288,21 +289,16 @@ def update_sensor_config( """Update an existing sensor config and ensure its sensors exist.""" sensor_config = ven_client.get_sensor_config(config_name) if sensor_config is None: - raise ValueError(f"Sensor config with name '{config_name}' not found.") + msg = f"Sensor config with name '{config_name}' not found." + raise ValueError(msg) sensor_config.name = form_data.name sensor_config.targets = list(form_data.targets) sensor_config.utc_trigger_time = form_data.utc_trigger_time - sensor_config.fetch_import_capacity_limits = ( - form_data.fetch_import_capacity_limits - ) - sensor_config.fetch_export_capacity_limits = ( - form_data.fetch_export_capacity_limits - ) + sensor_config.fetch_import_capacity_limits = form_data.fetch_import_capacity_limits + sensor_config.fetch_export_capacity_limits = form_data.fetch_export_capacity_limits - sensors = self._sensors.ensure_sensors_for_config( - ven_client.asset, sensor_config - ) + sensors = self._sensors.ensure_sensors_for_config(ven_client.asset, sensor_config) sensor_config.import_sensor = sensors.import_sensor sensor_config.export_sensor = sensors.export_sensor @@ -314,18 +310,15 @@ def delete_sensor_config(self, ven_client: VenClient, config_name: str) -> None: """Remove a sensor config and delete its associated sensors.""" sensor_config = ven_client.get_sensor_config(config_name) if sensor_config is None: - raise ValueError(f"Sensor config with name '{config_name}' not found.") + msg = f"Sensor config with name '{config_name}' not found." + raise ValueError(msg) self._sensors.delete_sensors_for_config(ven_client.asset, config_name) - ven_client.sensor_configs = [ - cfg for cfg in ven_client.sensor_configs if cfg.name != config_name - ] + ven_client.sensor_configs = [cfg for cfg in ven_client.sensor_configs if cfg.name != config_name] self._persist_payload(ven_client) db.session.flush() - def persist_job_id( - self, ven_client: VenClient, config_name: str, job_id: str | None - ) -> None: + def persist_job_id(self, ven_client: VenClient, config_name: str, job_id: str | None) -> None: """Store a job ID on a specific sensor config.""" sensor_config = ven_client.get_sensor_config(config_name) if sensor_config is None: @@ -363,9 +356,7 @@ def _strip_required_string_fields(cls, value: str) -> str: @field_validator("scopes", mode="before") @classmethod - def _parse_csv_string_fields( - cls, value: str | list[str] | tuple[str, ...] | None - ) -> list[str]: + def _parse_csv_string_fields(cls, value: str | list[str] | tuple[str, ...] | None) -> list[str]: if value is None: return [] if isinstance(value, str): @@ -376,6 +367,7 @@ def _parse_csv_string_fields( @classmethod def from_form_values(cls, field_values: VenClientFormValues) -> VenClientFormData: + """Build validated data from raw VEN client form values.""" return cls.model_validate(field_values.to_validation_input()) @@ -390,9 +382,7 @@ class VenSensorConfigFormData(BaseModel): @field_validator("targets", mode="before") @classmethod - def _parse_targets_csv( - cls, value: str | list[str] | tuple[str, ...] | None - ) -> list[str]: + def _parse_targets_csv(cls, value: str | list[str] | tuple[str, ...] | None) -> list[str]: if value is None: return [] if isinstance(value, str): @@ -403,20 +393,21 @@ def _parse_targets_csv( @field_validator("utc_trigger_time") @classmethod - def _validate_utc_trigger_time(cls, value: str, info: ValidationInfo) -> str: + def _validate_utc_trigger_time(cls, value: str, _info: ValidationInfo) -> str: parsed_value = value.strip() time_parts = parsed_value.split(":") - if len(time_parts) != 3 or not all(part.isdigit() for part in time_parts): - raise ValueError("Must be in UTC format HH:MM:SS.") + if len(time_parts) != _TIME_COMPONENT_COUNT or not all(part.isdigit() for part in time_parts): + msg = "Must be in UTC format HH:MM:SS." + raise ValueError(msg) hour, minute, second = (int(part) for part in time_parts) - if hour > 23 or minute > 59 or second > 59: - raise ValueError("Must be in UTC format HH:MM:SS.") + if hour > _MAX_HOUR or minute > _MAX_MINUTE or second > _MAX_SECOND: + msg = "Must be in UTC format HH:MM:SS." + raise ValueError(msg) return f"{hour:02d}:{minute:02d}:{second:02d}" @classmethod - def from_post_values( - cls, post_values: VenSensorConfigPostValues - ) -> VenSensorConfigFormData: + def from_post_values(cls, post_values: VenSensorConfigPostValues) -> VenSensorConfigFormData: + """Build validated data from parsed polling schedule POST values.""" return cls.model_validate(post_values.to_validation_input()) @@ -472,12 +463,8 @@ def build_ven_sensor_config_form_values( name=source.name, targets=", ".join(source.targets), utc_trigger_time=source.utc_trigger_time, - fetch_import_capacity_limits=( - "on" if source.fetch_import_capacity_limits else "" - ), - fetch_export_capacity_limits=( - "on" if source.fetch_export_capacity_limits else "" - ), + fetch_import_capacity_limits=("on" if source.fetch_import_capacity_limits else ""), + fetch_export_capacity_limits=("on" if source.fetch_export_capacity_limits else ""), ) @@ -497,14 +484,14 @@ def validate_ven_client_form( field_name = str(issue["loc"][-1]) errors.add(field_name, issue["msg"]) else: - assert ven_client_data is not None + if ven_client_data is None: + msg = "Validated VEN client form data missing despite successful validation." + raise RuntimeError(msg) existing = ven_client_repository.find_by_name(ven_client_data.name) if existing and ven_client_data.name != current_name: errors.add("name", "A VEN client with this name already exists.") - return FormValidationResult( - data=ven_client_data if not errors else None, errors=errors - ) + return FormValidationResult(data=ven_client_data if not errors else None, errors=errors) def validate_ven_sensor_config_form( @@ -523,16 +510,10 @@ def validate_ven_sensor_config_form( field_name = str(issue["loc"][-1]) errors.add(field_name, issue["msg"]) else: - assert config is not None - if not ( - config.fetch_import_capacity_limits or config.fetch_export_capacity_limits - ): - errors.add( - "limits", "Select at least one limit type (import and/or export)." - ) - - if existing_configs is not None and config.name != current_name: - if any(cfg.name == config.name for cfg in existing_configs): - errors.add("name", "A sensor config with this name already exists.") + if not (config.fetch_import_capacity_limits or config.fetch_export_capacity_limits): + errors.add("limits", "Select at least one limit type (import and/or export).") + + if existing_configs is not None and config.name != current_name and any(cfg.name == config.name for cfg in existing_configs): + errors.add("name", "A sensor config with this name already exists.") return FormValidationResult(data=config if not errors else None, errors=errors) diff --git a/flexmeasures_openadr3/utils/ven_jobs.py b/flexmeasures_openadr3/utils/ven_jobs.py index ec59188..4ad5788 100644 --- a/flexmeasures_openadr3/utils/ven_jobs.py +++ b/flexmeasures_openadr3/utils/ven_jobs.py @@ -1,11 +1,11 @@ from __future__ import annotations -from collections.abc import Iterator from dataclasses import dataclass -from datetime import datetime, timedelta, timezone -from typing import cast +from datetime import UTC, datetime, timedelta +from typing import TYPE_CHECKING, cast from flask import current_app +from flexmeasures.data import db # type: ignore[import-untyped] from flexmeasures.data.models.time_series import Sensor, TimedBelief from flexmeasures.data.services.data_sources import get_or_create_source from openadr3_client.oadr310._vtn.interfaces.filters import TargetFilter @@ -15,7 +15,6 @@ from rq.exceptions import NoSuchJobError from rq.job import Job -from flexmeasures.data import db # type: ignore[import-untyped] from flexmeasures_openadr3.models.jobs import EventActivePeriod from flexmeasures_openadr3.utils.ven_clients import ( VenClient, @@ -23,6 +22,9 @@ VenSensorConfig, ) +if TYPE_CHECKING: + from collections.abc import Iterator + OPENADR_EVENT_SOURCE_NAME = "OpenADR 3 VTN" OPENADR_EVENT_SOURCE_TYPE = "gateway" FETCH_EVENTS_QUEUE_NAME = "ingestion" @@ -44,22 +46,14 @@ class PayloadTypeSensorMapping: export_sensor: Sensor | None = None @classmethod - def from_sensor_config( - cls, sensor_config: VenSensorConfig - ) -> PayloadTypeSensorMapping: - import_sensor = ( - sensor_config.import_sensor - if sensor_config.fetch_import_capacity_limits - else None - ) - export_sensor = ( - sensor_config.export_sensor - if sensor_config.fetch_export_capacity_limits - else None - ) + def from_sensor_config(cls, sensor_config: VenSensorConfig) -> PayloadTypeSensorMapping: + """Build a mapping from a polling schedule's enabled limit types.""" + import_sensor = sensor_config.import_sensor if sensor_config.fetch_import_capacity_limits else None + export_sensor = sensor_config.export_sensor if sensor_config.fetch_export_capacity_limits else None return cls(import_sensor=import_sensor, export_sensor=export_sensor) def get_sensor_for(self, payload_type: EventPayloadType) -> Sensor | None: + """Return the FlexMeasures sensor for an OpenADR payload type.""" if payload_type is EventPayloadType.IMPORT_CAPACITY_LIMIT: return self.import_sensor if payload_type is EventPayloadType.EXPORT_CAPACITY_LIMIT: @@ -67,6 +61,7 @@ def get_sensor_for(self, payload_type: EventPayloadType) -> Sensor | None: return None def __bool__(self) -> bool: + """Return whether at least one sensor is configured.""" return self.import_sensor is not None or self.export_sensor is not None @@ -76,7 +71,7 @@ def _build_job_id(ven_id: int, config_name: str, run_at: datetime) -> str: def _next_daily_trigger_datetime(utc_trigger_time: str) -> datetime: hour, minute, second = (int(part) for part in utc_trigger_time.split(":")) - now_utc = datetime.now(timezone.utc) + now_utc = datetime.now(UTC) trigger = now_utc.replace(hour=hour, minute=minute, second=second, microsecond=0) if trigger <= now_utc: trigger += timedelta(days=1) @@ -84,7 +79,7 @@ def _next_daily_trigger_datetime(utc_trigger_time: str) -> datetime: def _fetch_events_queue() -> Queue: - return cast(Queue, current_app.queues[FETCH_EVENTS_QUEUE_NAME]) + return cast("Queue", current_app.queues[FETCH_EVENTS_QUEUE_NAME]) def _delete_job(job_id: str) -> None: @@ -96,20 +91,15 @@ def _delete_job(job_id: str) -> None: existing_job.delete() -def _get_interval_period( - event: ExistingEvent, interval: Interval -) -> tuple[datetime, datetime]: +def _get_interval_period(event: ExistingEvent, interval: Interval) -> tuple[datetime, datetime]: interval_period = interval.interval_period or event.interval_period if interval_period is None: - raise ValueError( - f"OpenADR event '{event.id}' interval '{interval.id}' has no interval period." - ) + msg = f"OpenADR event '{event.id}' interval '{interval.id}' has no interval period." + raise ValueError(msg) return interval_period.start, interval_period.start + interval_period.duration -def _iter_sensor_event_starts( - start: datetime, end: datetime, event_resolution: timedelta -) -> Iterator[datetime]: +def _iter_sensor_event_starts(start: datetime, end: datetime, event_resolution: timedelta) -> Iterator[datetime]: if event_resolution == timedelta(0): yield start return @@ -121,11 +111,7 @@ def _iter_sensor_event_starts( def _get_supported_event_payload_types(event: ExistingEvent) -> set[EventPayloadType]: - return { - pd.payload_type - for pd in (event.payload_descriptors or ()) - if pd.payload_type in SUPPORTED_DR_PAYLOAD_TYPES - } + return {pd.payload_type for pd in (event.payload_descriptors or ()) if pd.payload_type in SUPPORTED_DR_PAYLOAD_TYPES} def _collect_active_periods(events: list[ExistingEvent]) -> list[EventActivePeriod]: @@ -133,26 +119,23 @@ def _collect_active_periods(events: list[ExistingEvent]) -> list[EventActivePeri for event in events: for interval in event.intervals or (): start, end = _get_interval_period(event, interval) - active_periods.append( - EventActivePeriod(start=start, end=end, event_id=event.id) - ) + active_periods.append(EventActivePeriod(start=start, end=end, event_id=event.id)) return active_periods def _validate_no_overlapping_events(events: list[ExistingEvent]) -> None: - active_periods = sorted( - _collect_active_periods(events), key=lambda period: period.start - ) + active_periods = sorted(_collect_active_periods(events), key=lambda period: period.start) for index, period in enumerate(active_periods): for previous in active_periods[:index]: if previous.event_id == period.event_id: continue if previous.start < period.end and period.start < previous.end: - raise OverlappingDemandResponseEventsError( + msg = ( f"OpenADR events overlap: " f"event '{previous.event_id}' active from {previous.start} to {previous.end}, " f"event '{period.event_id}' active from {period.start} to {period.end}." ) + raise OverlappingDemandResponseEventsError(msg) def _fetch_events(ven_client: VenClient, targets: list[str]) -> list[ExistingEvent]: @@ -186,14 +169,10 @@ def _store_event_payloads( if sensor is None: continue if len(payload.values) != 1: - raise ValueError( - f"OpenADR event '{event.id}' interval '{interval.id}' " - f"payload '{payload.type}' has {len(payload.values)} values." - ) + msg = f"OpenADR event '{event.id}' interval '{interval.id}' payload '{payload.type}' has {len(payload.values)} values." + raise ValueError(msg) - for belief_start in _iter_sensor_event_starts( - start, end, sensor.event_resolution - ): + for belief_start in _iter_sensor_event_starts(start, end, sensor.event_resolution): db.session.merge( TimedBelief( sensor=sensor, @@ -218,6 +197,7 @@ def schedule( self, ven_client: VenClient, sensor_config: VenSensorConfig, + *, replace_existing: bool = True, ) -> Job | None: """Schedule or reschedule the daily fetch-events job for a sensor config.""" @@ -249,7 +229,7 @@ def schedule( ) self._repository.persist_job_id(ven_client, sensor_config.name, job.id) - return cast(Job, job) + return cast("Job", job) def delete(self, sensor_config: VenSensorConfig) -> None: """Delete the scheduled job for a single sensor config.""" @@ -265,9 +245,7 @@ def _execute(self, ven_id: int, config_name: str) -> None: """Fetch events for a sensor config and store the resulting beliefs.""" ven_client = self._repository.find_by_id(ven_id) if ven_client is None: - current_app.logger.warning( - "Stopping fetch-events job: VEN id '%s' no longer exists.", ven_id - ) + current_app.logger.warning("Stopping fetch-events job: VEN id '%s' no longer exists.", ven_id) return sensor_config = ven_client.get_sensor_config(config_name) diff --git a/pyproject.toml b/pyproject.toml index c61bed2..3e1dcf3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,9 +82,45 @@ warn_return_any = true check_untyped_defs = true [[tool.mypy.overrides]] -module = ["flask_login.*", "flexmeasures.*", "tests.*", "testcontainers.*"] +module = ["flask_login.*", "flexmeasures.*", "tests.*", "testcontainers.*", "settings"] ignore_missing_imports = true disallow_untyped_defs = false [tool.ruff] -# Your ruff config here +line-length = 175 + +[tool.ruff.lint] +select = ["ALL"] + +# Do not require docstrings for functions, methods and classes +ignore = [ + # From:https://github.com/wemake-services/wemake-django-template/blob/master/pyproject.toml + "A005", # allow to shadow stdlib and builtin module names + "COM812", # trailing comma, conflicts with `ruff format` + # Different doc rules that we don't really care about: + "D100", + "D104", + "D106", + "D203", + "D212", + "D401", + "D404", + "D405", + "ISC001", # implicit string concat conflicts with `ruff format` + "ISC003", # prefer explicit string concat over implicit concat + "PLR09", # we have our own complexity rules + # "PLR2004", # do not report magic numbers -> I do want to report magic numbers + "PLR6301", # do not require classmethod / staticmethod when self not used + "TRY003", # long exception messages from `tryceratops` + # Start of own error codes + "ANN002", # Ignore unused *args + "ANN003", # Ignore unused **kwargs + "D107", # Allow undocumented __init__ + "TC001", # Move to type checking block (does not play nice with pydantic models) + "TC002", # Move to type checking block (does not play nice with pydantic models) +] + +# Ignores S101 ("Use of 'assert' detected") for the tests. +[tool.ruff.lint.per-file-ignores] +"tests/*" = ["S101", "ANN201", "SLF001", "S106", "S105", "S311", "PLR2004", "PT018"] +"examples/**" = ["EXE001", "T201", "S311", "S105", "INP001", "SLF001"] \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 8b1eb7f..e4b778b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,14 +2,18 @@ from __future__ import annotations -from collections.abc import Iterator +import os +from pathlib import Path +from typing import TYPE_CHECKING import pytest from flask import Flask from flask_login import login_user from flask_sqlalchemy import SQLAlchemy - from flexmeasures.data.models.user import User +from openadr3_client.oadr310.models.event.event import ExistingEvent +from testcontainers.core.network import Network +from testcontainers.keycloak import KeycloakContainer from flexmeasures_openadr3.utils.ven_clients import ( VenClient, @@ -18,13 +22,6 @@ VenSensorConfigFormData, ) from flexmeasures_openadr3.utils.ven_jobs import VenFetchJobScheduler - -import os -from pathlib import Path -from typing import Iterable -from openadr3_client.oadr310.models.event.event import ExistingEvent -from testcontainers.core.network import Network -from testcontainers.keycloak import KeycloakContainer from tests.test_container.integration_types import ( IntegrationTestVTNClient, IntegrationTestVTNServer, @@ -38,6 +35,8 @@ seed_capacity_limit_event, ) +if TYPE_CHECKING: + from collections.abc import Iterable, Iterator _TESTS_ROOT = Path(__file__).resolve().parents[0] _KEYCLOAK_REALM_IMPORT_FILE = _TESTS_ROOT / "keycloak_integration_realm.json" @@ -52,9 +51,7 @@ KEYCLOAK_VEN2_CLIENT_ID = "test-ven-2" KEYCLOAK_VEN_CLIENT_SECRET = "my-client-secret" -KEYCLOAK_INTERNAL_BASE_URL = ( - f"http://keycloak:8080/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect" -) +KEYCLOAK_INTERNAL_BASE_URL = f"http://keycloak:8080/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect" KEYCLOAK_INTERNAL_TOKEN_URL = f"{KEYCLOAK_INTERNAL_BASE_URL}/token" KEYCLOAK_INTERNAL_JWKS_URL = f"{KEYCLOAK_INTERNAL_BASE_URL}/certs" @@ -142,16 +139,16 @@ def logged_in_prosumer( @pytest.fixture def created_ven_client( - fresh_db: object, + fresh_db: object, # noqa: ARG001 db: SQLAlchemy, - logged_in_prosumer: User, + logged_in_prosumer: User, # noqa: ARG001 ven_client_repository: VenClientRepository, sample_ven_client_form_data: VenClientFormData, -) -> Iterator[VenClient]: +) -> VenClient: """VEN client persisted in the database.""" ven_client = ven_client_repository.create(sample_ven_client_form_data) db.session.commit() - yield ven_client + return ven_client @pytest.fixture @@ -160,7 +157,7 @@ def created_ven_client_with_schedule( created_ven_client: VenClient, ven_client_repository: VenClientRepository, sample_sensor_config_form_data: VenSensorConfigFormData, -) -> Iterator[VenClient]: +) -> VenClient: """VEN client with one polling schedule and linked sensors.""" sensor_config = ven_client_repository.append_sensor_config( created_ven_client, @@ -168,7 +165,7 @@ def created_ven_client_with_schedule( ) db.session.commit() assert sensor_config is not None - yield created_ven_client + return created_ven_client @pytest.fixture(scope="session") @@ -227,10 +224,7 @@ def integration_test_oauth_client_bl_client( IntegrationTestOAuthClient: The integration test oauth client. """ - token_url = ( - integration_test_auth_server.get_url() - + f"/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect/token" - ) + token_url = integration_test_auth_server.get_url() + f"/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect/token" return OAuthConfiguration( client_id=KEYCLOAK_BL_CLIENT_ID, client_secret=KEYCLOAK_BL_CLIENT_SECRET, @@ -279,10 +273,7 @@ def bl_oauth_configuration( Yields an OAuthConfiguration which contains an oauth client that was created for the scope of this test session. This OAUTH client is configured to have the BL scopes inside an OpenADR VTN. """ - token_url = ( - integration_test_auth_server.get_url() - + f"/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect/token" - ) + token_url = integration_test_auth_server.get_url() + f"/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect/token" return OAuthConfiguration( client_id=KEYCLOAK_BL_CLIENT_ID, client_secret=KEYCLOAK_BL_CLIENT_SECRET, @@ -302,10 +293,7 @@ def ven_oauth_configuration( Yields an OAuthConfiguration which contains an oauth client that was created for the scope of this test session. This OAUTH client is configured to have the VEN scopes inside an OpenADR VTN. """ - token_url = ( - integration_test_auth_server.get_url() - + f"/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect/token" - ) + token_url = integration_test_auth_server.get_url() + f"/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect/token" return OAuthConfiguration( client_id=KEYCLOAK_VEN1_CLIENT_ID, client_secret=KEYCLOAK_VEN_CLIENT_SECRET, @@ -321,8 +309,7 @@ def bl_client( bl_oauth_configuration: OAuthConfiguration, ) -> IntegrationTestVTNClient: """ - Returns an IntegrationTestVTNClient which contains VTN server information and an OAUTH configuration which can be used - to fetch a BL access token to communicate with the VTN. + Return a BL-side VTN client with server info and OAuth credentials. Args: integration_test_openadr310_vtn_server (IntegrationTestVTNServer): OpenLEADR-rs VTN server. @@ -344,8 +331,7 @@ def ven_client( ven_oauth_configuration: OAuthConfiguration, ) -> IntegrationTestVTNClient: """ - Returns an IntegrationTestVTNClient which contains VTN server information and an OAUTH configuration which can be used - to fetch a VEN access token to communicate with the VTN. + Return a VEN-side VTN client with server info and OAuth credentials. Args: integration_test_openadr310_vtn_server (IntegrationTestVTNServer): OpenLEADR-rs VTN server. @@ -367,7 +353,6 @@ def vtn_capacity_limit_event_seed( ) -> ExistingEvent: """Program + 24h capacity-limit event on the VTN, starting one hour from seed time.""" bl_http = create_bl_http_client(bl_client) - seeded_event = seed_capacity_limit_event( + return seed_capacity_limit_event( bl_http, ) - return seeded_event diff --git a/tests/test_container/conftest.py b/tests/test_container/conftest.py index 1011b43..d825edd 100644 --- a/tests/test_container/conftest.py +++ b/tests/test_container/conftest.py @@ -1,10 +1,9 @@ import os -from collections.abc import Iterator +from collections.abc import Iterable, Iterator from pathlib import Path -from typing import Iterable -from openadr3_client.oadr310.models.event.event import ExistingEvent -import pytest +import pytest +from openadr3_client.oadr310.models.event.event import ExistingEvent from testcontainers.core.network import Network from testcontainers.keycloak import KeycloakContainer @@ -34,9 +33,7 @@ KEYCLOAK_VEN2_CLIENT_ID = "test-ven-2" KEYCLOAK_VEN_CLIENT_SECRET = "my-client-secret" -KEYCLOAK_INTERNAL_BASE_URL = ( - f"http://keycloak:8080/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect" -) +KEYCLOAK_INTERNAL_BASE_URL = f"http://keycloak:8080/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect" KEYCLOAK_INTERNAL_TOKEN_URL = f"{KEYCLOAK_INTERNAL_BASE_URL}/token" KEYCLOAK_INTERNAL_JWKS_URL = f"{KEYCLOAK_INTERNAL_BASE_URL}/certs" @@ -116,10 +113,7 @@ def integration_test_oauth_client_bl_client( IntegrationTestOAuthClient: The integration test oauth client. """ - token_url = ( - integration_test_auth_server.get_url() - + f"/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect/token" - ) + token_url = integration_test_auth_server.get_url() + f"/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect/token" return OAuthConfiguration( client_id=KEYCLOAK_BL_CLIENT_ID, client_secret=KEYCLOAK_BL_CLIENT_SECRET, @@ -168,10 +162,7 @@ def bl_oauth_configuration( Yields an OAuthConfiguration which contains an oauth client that was created for the scope of this test session. This OAUTH client is configured to have the BL scopes inside an OpenADR VTN. """ - token_url = ( - integration_test_auth_server.get_url() - + f"/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect/token" - ) + token_url = integration_test_auth_server.get_url() + f"/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect/token" return OAuthConfiguration( client_id=KEYCLOAK_BL_CLIENT_ID, client_secret=KEYCLOAK_BL_CLIENT_SECRET, @@ -191,10 +182,7 @@ def ven_oauth_configuration( Yields an OAuthConfiguration which contains an oauth client that was created for the scope of this test session. This OAUTH client is configured to have the VEN scopes inside an OpenADR VTN. """ - token_url = ( - integration_test_auth_server.get_url() - + f"/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect/token" - ) + token_url = integration_test_auth_server.get_url() + f"/realms/{KEYCLOAK_REALM_NAME}/protocol/openid-connect/token" return OAuthConfiguration( client_id=KEYCLOAK_VEN1_CLIENT_ID, client_secret=KEYCLOAK_VEN_CLIENT_SECRET, @@ -210,8 +198,7 @@ def bl_client( bl_oauth_configuration: OAuthConfiguration, ) -> IntegrationTestVTNClient: """ - Returns an IntegrationTestVTNClient which contains VTN server information and an OAUTH configuration which can be used - to fetch a BL access token to communicate with the VTN. + Return a BL-side VTN client with server info and OAuth credentials. Args: integration_test_openadr310_vtn_server (IntegrationTestVTNServer): OpenLEADR-rs VTN server. @@ -233,8 +220,7 @@ def ven_client( ven_oauth_configuration: OAuthConfiguration, ) -> IntegrationTestVTNClient: """ - Returns an IntegrationTestVTNClient which contains VTN server information and an OAUTH configuration which can be used - to fetch a VEN access token to communicate with the VTN. + Return a VEN-side VTN client with server info and OAuth credentials. Args: integration_test_openadr310_vtn_server (IntegrationTestVTNServer): OpenLEADR-rs VTN server. @@ -256,7 +242,6 @@ def vtn_capacity_limit_event_seed( ) -> ExistingEvent: """Program + 24h capacity-limit event on the VTN, starting one hour from seed time.""" bl_http = create_bl_http_client(bl_client) - seeded_event = seed_capacity_limit_event( + return seed_capacity_limit_event( bl_http, ) - return seeded_event diff --git a/tests/test_container/flexmeasures_test_postgres_container.py b/tests/test_container/flexmeasures_test_postgres_container.py index ca1d453..e516701 100644 --- a/tests/test_container/flexmeasures_test_postgres_container.py +++ b/tests/test_container/flexmeasures_test_postgres_container.py @@ -3,15 +3,17 @@ from __future__ import annotations from dataclasses import dataclass -from types import TracebackType -from typing import Self +from typing import TYPE_CHECKING, Self from testcontainers.postgres import PostgresContainer +if TYPE_CHECKING: + from types import TracebackType + FLEXMEASURES_TEST_POSTGRES_IMAGE = "postgres" FLEXMEASURES_TEST_DB_NAME = "flexmeasures_test" FLEXMEASURES_TEST_DB_USER = "flexmeasures_test" -FLEXMEASURES_TEST_DB_PASSWORD = "flexmeasures_test" # noqa: S105 +FLEXMEASURES_TEST_DB_PASSWORD = "flexmeasures_test" @dataclass(frozen=True) @@ -39,10 +41,12 @@ def __init__(self) -> None: self._container = container def start(self) -> Self: + """Start the PostgreSQL container.""" self._container.start() return self def stop(self) -> None: + """Stop the PostgreSQL container.""" self._container.stop() def _get_database_uri(self) -> str: @@ -52,6 +56,7 @@ def _get_database_uri(self) -> str: return url.replace("postgresql+psycopg2://", "postgresql://", 1) def get_connection_info(self) -> FlexMeasuresTestPostgres: + """Return connection details for the running test database.""" host = self._container.get_container_host_ip() port = int(self._container.get_exposed_port(self._container.port)) return FlexMeasuresTestPostgres( @@ -64,6 +69,7 @@ def get_connection_info(self) -> FlexMeasuresTestPostgres: ) def __enter__(self) -> Self: + """Enter the container context manager.""" return self.start() def __exit__( @@ -72,4 +78,5 @@ def __exit__( exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: + """Exit the container context manager.""" self.stop() diff --git a/tests/test_container/integration_types.py b/tests/test_container/integration_types.py index c484510..8003e4d 100644 --- a/tests/test_container/integration_types.py +++ b/tests/test_container/integration_types.py @@ -7,6 +7,8 @@ @dataclass class OAuthConfiguration: + """OAuth client credentials and token endpoint settings.""" + client_id: str client_secret: str token_url: str @@ -16,11 +18,15 @@ class OAuthConfiguration: @dataclass class IntegrationTestVTNServer: + """Contains information on the VTN server used during tests.""" + vtn_base_url: str allow_insecure_http: bool @dataclass class IntegrationTestVTNClient: + """Contains information on the VTN client used during tests.""" + vtn_configuration: IntegrationTestVTNServer oauth_configuration: OAuthConfiguration diff --git a/tests/test_container/oadr310_vtn_test_container.py b/tests/test_container/oadr310_vtn_test_container.py index a9317e2..30be88f 100644 --- a/tests/test_container/oadr310_vtn_test_container.py +++ b/tests/test_container/oadr310_vtn_test_container.py @@ -4,10 +4,10 @@ import re import threading +import uuid from collections import deque from types import TracebackType from typing import Any, Self -import uuid import docker.errors from testcontainers.core.container import DockerContainer, LogMessageWaitStrategy @@ -84,9 +84,7 @@ def __init__( ) .with_network(self._network) .with_network_aliases(self._postgres_alias) - .waiting_for( - LogMessageWaitStrategy("database system is ready to accept connections") - ) + .waiting_for(LogMessageWaitStrategy("database system is ready to accept connections")) ) # Initialize VTN container with the static environment variables. @@ -107,11 +105,7 @@ def __init__( .with_env(key="PG_TZ", value="Europe/Amsterdam") .with_env(key="RUST_BACKTRACE", value="full") .with_env(key="RUST_LOG", value="trace") - .waiting_for( - LogMessageWaitStrategy( - re.compile(r".*ListStore\.__init__\(\).*"), re.DOTALL - ) - ) + .waiting_for(LogMessageWaitStrategy(re.compile(r".*ListStore\.__init__\(\).*"), re.DOTALL)) ) def _start_vtn_log_capture(self) -> None: @@ -123,16 +117,10 @@ def _start_vtn_log_capture(self) -> None: def _run() -> None: try: - for chunk in wrapped.logs( - stream=True, follow=True, stdout=True, stderr=True - ): + for chunk in wrapped.logs(stream=True, follow=True, stdout=True, stderr=True): if self._log_stop_event.is_set(): break - chunk_bytes = ( - chunk - if isinstance(chunk, bytes | bytearray) - else str(chunk).encode("utf-8", errors="replace") - ) + chunk_bytes = chunk if isinstance(chunk, bytes | bytearray) else str(chunk).encode("utf-8", errors="replace") text = chunk_bytes.decode("utf-8", errors="replace") for line in text.splitlines(): if self._log_stop_event.is_set(): @@ -141,9 +129,7 @@ def _run() -> None: except (docker.errors.DockerException, OSError): return - self._vtn_log_thread = threading.Thread( - target=_run, name="oadr310-ref-vtn-logs", daemon=True - ) + self._vtn_log_thread = threading.Thread(target=_run, name="oadr310-ref-vtn-logs", daemon=True) self._vtn_log_thread.start() def get_vtn_log_tail(self, max_lines: int = 500) -> list[str]: @@ -172,9 +158,7 @@ def start(self) -> Self: ) # Configure the VTN with the MQTT broker URL prior to starting it. - self._vtn.with_env("DATABASE_URL", value=vtn_db_url).waiting_for( - LogMessageWaitStrategy("pg_advisory_unlock") - ).start() + self._vtn.with_env("DATABASE_URL", value=vtn_db_url).waiting_for(LogMessageWaitStrategy("pg_advisory_unlock")).start() self._start_vtn_log_capture() return self diff --git a/tests/test_container/openadr_vtn_setup.py b/tests/test_container/openadr_vtn_setup.py index 073aa6a..86cc7dc 100644 --- a/tests/test_container/openadr_vtn_setup.py +++ b/tests/test_container/openadr_vtn_setup.py @@ -2,11 +2,11 @@ from __future__ import annotations -from datetime import datetime, timedelta, timezone import random -from typing import Tuple import uuid +from datetime import UTC, datetime, timedelta +from openadr3_client._models.common.interval_period import IntervalPeriod from openadr3_client.bl.http_factory import BusinessLogicHttpClientFactory from openadr3_client.oadr310._bl.client import BusinessLogicClient from openadr3_client.oadr310._ven.client import VirtualEndNodeClient @@ -19,7 +19,6 @@ from openadr3_client.oadr310.models.program.program import NewProgram from openadr3_client.oadr310.models.unit import Unit from openadr3_client.ven.http_factory import VirtualEndNodeHttpClientFactory -from openadr3_client._models.common.interval_period import IntervalPeriod from openadr3_client.version import OADRVersion from tests.test_container.integration_types import IntegrationTestVTNClient @@ -73,7 +72,7 @@ def seed_capacity_limit_event( export capacity limit value. FlexMeasures stores one belief per interval at the interval start when the VEN fetch job runs. """ - event_start = datetime.now(timezone.utc).replace(microsecond=0) + timedelta(hours=1) + event_start = datetime.now(UTC).replace(microsecond=0) + timedelta(hours=1) event_duration = timedelta(hours=24) payload_descriptors = ( EventPayloadDescriptor( @@ -93,7 +92,7 @@ def seed_capacity_limit_event( ) ) - intervals: Tuple[Interval[EventPayload], ...] = () + intervals: tuple[Interval[EventPayload], ...] = () for i in range(event_duration // timedelta(minutes=15)): intervals += ( diff --git a/tests/test_ven_client_repository.py b/tests/test_ven_client_repository.py index 073360e..678264d 100644 --- a/tests/test_ven_client_repository.py +++ b/tests/test_ven_client_repository.py @@ -25,7 +25,6 @@ def test_create_ven_client_persists_connection_attributes( fresh_db: SQLAlchemy, - logged_in_prosumer: object, ven_client_repository: VenClientRepository, sample_ven_client_form_data: VenClientFormData, ) -> None: @@ -47,7 +46,6 @@ def test_create_ven_client_persists_connection_attributes( def test_create_ven_client_encrypts_oauth_credentials_in_attributes( fresh_db: SQLAlchemy, - logged_in_prosumer: object, ven_client_repository: VenClientRepository, sample_ven_client_form_data: VenClientFormData, ) -> None: @@ -60,14 +58,8 @@ def test_create_ven_client_encrypts_oauth_credentials_in_attributes( assert raw["oauth_client_secret"] != sample_ven_client_form_data.oauth_client_secret encryptor = SecretsEncryptor.from_current_app() - assert ( - encryptor.decrypt(raw["oauth_client_id"]) - == sample_ven_client_form_data.oauth_client_id - ) - assert ( - encryptor.decrypt(raw["oauth_client_secret"]) - == sample_ven_client_form_data.oauth_client_secret - ) + assert encryptor.decrypt(raw["oauth_client_id"]) == sample_ven_client_form_data.oauth_client_id + assert encryptor.decrypt(raw["oauth_client_secret"]) == sample_ven_client_form_data.oauth_client_secret def test_append_sensor_config_persists_polling_schedule( @@ -83,14 +75,8 @@ def test_append_sensor_config_persists_polling_schedule( assert config is not None assert config.targets == sample_sensor_config_form_data.targets assert config.utc_trigger_time == sample_sensor_config_form_data.utc_trigger_time - assert ( - config.fetch_import_capacity_limits - == sample_sensor_config_form_data.fetch_import_capacity_limits - ) - assert ( - config.fetch_export_capacity_limits - == sample_sensor_config_form_data.fetch_export_capacity_limits - ) + assert config.fetch_import_capacity_limits == sample_sensor_config_form_data.fetch_import_capacity_limits + assert config.fetch_export_capacity_limits == sample_sensor_config_form_data.fetch_export_capacity_limits payload = VenClientAttributePayload.from_asset_attributes(reloaded.asset.attributes) assert len(payload.sensor_configs) == 1 @@ -171,8 +157,6 @@ def test_delete_sensor_config_removes_schedule_from_attributes( def test_duplicate_ven_client_name_raises( - created_ven_client: VenClient, - logged_in_prosumer: object, ven_client_repository: VenClientRepository, sample_ven_client_form_data: VenClientFormData, ) -> None: diff --git a/tests/test_ven_fetch_e2e.py b/tests/test_ven_fetch_e2e.py index c997b60..4c9842e 100644 --- a/tests/test_ven_fetch_e2e.py +++ b/tests/test_ven_fetch_e2e.py @@ -2,16 +2,14 @@ from __future__ import annotations - -from openadr3_client.oadr310.models.event.event import ExistingEvent -from openadr3_client.oadr310.models.event.event_payload import EventPayloadType import pytest from flask import Flask from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import select - from flexmeasures.data.models.time_series import TimedBelief from flexmeasures.data.services.data_sources import get_or_create_source +from openadr3_client.oadr310.models.event.event import ExistingEvent +from openadr3_client.oadr310.models.event.event_payload import EventPayloadType +from sqlalchemy import select from flexmeasures_openadr3.utils.ven_clients import ( VenClientFormData, @@ -23,7 +21,6 @@ OPENADR_EVENT_SOURCE_TYPE, VenFetchJobScheduler, ) - from tests.test_container.integration_types import IntegrationTestVTNClient @@ -58,19 +55,12 @@ def _beliefs_for_sensor(db: SQLAlchemy, sensor_id: int) -> list[TimedBelief]: source=OPENADR_EVENT_SOURCE_NAME, source_type=OPENADR_EVENT_SOURCE_TYPE, ) - return list( - db.session.scalars( - select(TimedBelief) - .filter_by(sensor_id=sensor_id, source_id=source.id) - .order_by(TimedBelief.event_start) - ).all() - ) + return list(db.session.scalars(select(TimedBelief).filter_by(sensor_id=sensor_id, source_id=source.id).order_by(TimedBelief.event_start)).all()) def test_ven_fetch_events_e2e_stores_openadr_capacity_limits( app: Flask, fresh_db: SQLAlchemy, - logged_in_prosumer: object, ven_client: IntegrationTestVTNClient, vtn_capacity_limit_event_seed: ExistingEvent, ven_client_repository: VenClientRepository, @@ -122,18 +112,10 @@ def test_ven_fetch_events_e2e_stores_openadr_capacity_limits( for i, belief in enumerate(import_beliefs): interval = seed.intervals[i] if seed.intervals is not None else None assert interval is not None and interval.interval_period is not None - assert ( - interval.interval_period.start - <= belief.event_start - < interval.interval_period.start + interval.interval_period.duration - ) + assert interval.interval_period.start <= belief.event_start < interval.interval_period.start + interval.interval_period.duration import_capacity_payload = next( - ( - payload - for payload in interval.payloads - if payload.type == EventPayloadType.IMPORT_CAPACITY_LIMIT - ), + (payload for payload in interval.payloads if payload.type == EventPayloadType.IMPORT_CAPACITY_LIMIT), None, ) assert import_capacity_payload is not None @@ -142,18 +124,10 @@ def test_ven_fetch_events_e2e_stores_openadr_capacity_limits( for i, belief in enumerate(export_beliefs): interval = seed.intervals[i] if seed.intervals is not None else None assert interval is not None and interval.interval_period is not None - assert ( - interval.interval_period.start - <= belief.event_start - < interval.interval_period.start + interval.interval_period.duration - ) + assert interval.interval_period.start <= belief.event_start < interval.interval_period.start + interval.interval_period.duration export_capacity_payload = next( - ( - payload - for payload in interval.payloads - if payload.type == EventPayloadType.EXPORT_CAPACITY_LIMIT - ), + (payload for payload in interval.payloads if payload.type == EventPayloadType.EXPORT_CAPACITY_LIMIT), None, ) assert export_capacity_payload is not None diff --git a/tests/test_ven_fetch_jobs.py b/tests/test_ven_fetch_jobs.py index 95b3166..e13119a 100644 --- a/tests/test_ven_fetch_jobs.py +++ b/tests/test_ven_fetch_jobs.py @@ -5,6 +5,7 @@ import pytest from flask import Flask from flask_sqlalchemy import SQLAlchemy +from rq.exceptions import NoSuchJobError from rq.job import Job from flexmeasures_openadr3.utils.ven_clients import ( @@ -21,7 +22,6 @@ def test_schedule_enqueues_job_in_forecasting_queue( app: Flask, fresh_db: SQLAlchemy, - clean_forecasting_queue: None, created_ven_client_with_schedule: VenClient, ven_client_repository: VenClientRepository, ven_fetch_job_scheduler: VenFetchJobScheduler, @@ -40,9 +40,7 @@ def test_schedule_enqueues_job_in_forecasting_queue( assert job.id is not None assert job.origin == FETCH_EVENTS_QUEUE_NAME - queued_job = Job.fetch( - job.id, connection=app.queues[FETCH_EVENTS_QUEUE_NAME].connection - ) + queued_job = Job.fetch(job.id, connection=app.queues[FETCH_EVENTS_QUEUE_NAME].connection) assert queued_job.id == job.id assert queued_job.kwargs == { "ven_id": reloaded.id, @@ -52,7 +50,6 @@ def test_schedule_enqueues_job_in_forecasting_queue( def test_schedule_persists_job_id_on_sensor_config( fresh_db: SQLAlchemy, - clean_forecasting_queue: None, created_ven_client_with_schedule: VenClient, ven_client_repository: VenClientRepository, ven_fetch_job_scheduler: VenFetchJobScheduler, @@ -100,7 +97,6 @@ def test_schedule_without_utc_trigger_time_returns_none( def test_delete_removes_scheduled_job_from_queue( app: Flask, fresh_db: SQLAlchemy, - clean_forecasting_queue: None, created_ven_client_with_schedule: VenClient, ven_client_repository: VenClientRepository, ven_fetch_job_scheduler: VenFetchJobScheduler, @@ -123,8 +119,6 @@ def test_delete_removes_scheduled_job_from_queue( ven_fetch_job_scheduler.delete(config_with_job) - from rq.exceptions import NoSuchJobError - with pytest.raises(NoSuchJobError): Job.fetch(job.id, connection=app.queues[FETCH_EVENTS_QUEUE_NAME].connection) @@ -132,7 +126,6 @@ def test_delete_removes_scheduled_job_from_queue( def test_delete_all_removes_jobs_for_every_schedule( app: Flask, fresh_db: SQLAlchemy, - clean_forecasting_queue: None, created_ven_client_with_schedule: VenClient, ven_client_repository: VenClientRepository, ven_fetch_job_scheduler: VenFetchJobScheduler, @@ -152,8 +145,6 @@ def test_delete_all_removes_jobs_for_every_schedule( ven_fetch_job_scheduler.delete_all(persisted) - from rq.exceptions import NoSuchJobError - for job_id in job_ids: assert job_id is not None with pytest.raises(NoSuchJobError): From 38c49399bb0dda17637f0b3f63e444d3e8ae9746 Mon Sep 17 00:00:00 2001 From: Nick van der Burgt Date: Tue, 23 Jun 2026 10:15:24 +0200 Subject: [PATCH 07/13] Set credits to elaad and foss email --- .../ui/templates/base_flexmeasures_openadr3.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flexmeasures_openadr3/ui/templates/base_flexmeasures_openadr3.html b/flexmeasures_openadr3/ui/templates/base_flexmeasures_openadr3.html index e12be7b..f4789f4 100755 --- a/flexmeasures_openadr3/ui/templates/base_flexmeasures_openadr3.html +++ b/flexmeasures_openadr3/ui/templates/base_flexmeasures_openadr3.html @@ -1,6 +1,6 @@ {% extends "base.html" %} {% block copyright_notice %} -This is a plugin created by Nick van der Burgt. +This is a plugin created by Stichting ElaadNL. . {% endblock copyright_notice %} From 4816df4b196de40bebdabe6fa17990c46cd91b1d Mon Sep 17 00:00:00 2001 From: Nick van der Burgt Date: Tue, 23 Jun 2026 10:51:40 +0200 Subject: [PATCH 08/13] Remove certs --- tests/certs/oadr310/vtn/cert.pem | 29 ------------------ tests/certs/oadr310/vtn/key.pem | 52 -------------------------------- 2 files changed, 81 deletions(-) delete mode 100644 tests/certs/oadr310/vtn/cert.pem delete mode 100644 tests/certs/oadr310/vtn/key.pem diff --git a/tests/certs/oadr310/vtn/cert.pem b/tests/certs/oadr310/vtn/cert.pem deleted file mode 100644 index b0238a4..0000000 --- a/tests/certs/oadr310/vtn/cert.pem +++ /dev/null @@ -1,29 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFCTCCAvGgAwIBAgIUFtSO6/PUSl27AYkhqzBoK7hAm5AwDQYJKoZIhvcNAQEL -BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MTIwMzA3NTExNloXDTI2MTIw -MzA3NTExNlowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF -AAOCAg8AMIICCgKCAgEAzbebmcxvxTsyYeVSjM0hIZAf84f/QrCvuYU3M0GNLVMU -vNRp4VGmVi9eCa/mz0a9xYnyXujP3LlS2O4SN66TyMT3xP9F6xSTmIv+K2B0GZeT -XCCjiVdNWlm+rlWOi+I9Th4XCpMCDUTIPa1mxJ3KWal/7SwoFbhGkYiuqVH6gjEq -vtmTNamsPXxc+OayMpAbxZc1PQcYAs6bYOgX6uqG6iZb0a9u+DMErtS0t3Aa9MTg -ZtV3ZsxBerlrOq2fCmJzw6NDQHjAw+3wfuut04W0BDhmVuRLYtTn/HGDNWWh2Hip -6pRqpAp1eEOf6hJZOQSVmrtvHpnQoMMQpP2J3MuMHLTxHbh5x93sgkYRtdG1GrnB -ZuxBEDIyj2mOgmf8bZurXI2d3FyyqRgiuKKFfn9iXArIpt9yiGQIyOHVYPnyRSQK -5MLar3xWK0QeaT5SnWMBAuuKve4L+v2lYr94QRXqMEHuYHCafZhJ/+rjb4qys3tM -YS1kDuxo2zcchU7Srk4uyrnTNXSnJaypTVsql4fGtEmgJXvtmjZS3ELm6SwT38/N -qnOR+jztemTti16i375mILvegv5MUGRAbiL4So+NrBkn7AcsfPoRYfnfYdJFWtD+ -p5P2mwB+KJfGR8Z1UiVQhErZo0LI1eP/cpr1cNj82vWhbC0VnLkXoDFpNwfT3qEC -AwEAAaNTMFEwHQYDVR0OBBYEFLvWqMhBiotM9EX8rizjjozbSYtgMB8GA1UdIwQY -MBaAFLvWqMhBiotM9EX8rizjjozbSYtgMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI -hvcNAQELBQADggIBAEt7urhiA07EpZeuSQwKxTOivrX24TrVCz/GOBBxFAu9xRJQ -ID8KYd8picLQOMxiN3adNKxoY3W5oNuAJNrZVMnCUg31RXolSdQmvyXCPOkqKGP3 -5Faod/O9Gh/r+8ctwtpg2zIUbpuIcKYXKQ1MYwBdhVyKfB/I1nKmD8ysTDX5dHWr -YjMVKjeTeKuLkqdXbR6dFbdcf8ZTjLk7d9xaRRn+GaXa9Xn/XGx8S3eXM/RlfijT -3t3CcLfXjpQGl/gB3YXPQty1XXJYEZRwgyMMD0rBUdWaL/INAHdoTOaTlpWJK+gw -fLFovEVeWA6GeZKn+pGZPIS4q7K04H94W2lsGiCqMiRScQ5/ITEGX9SOGKmKFPyz -fe+wNfazpSJTn8rsyGwGGRx/9t5/kVZRBkXJ+GGol8BN+DhKWER70HaSVCCeyy+D -3TsYO3LecJWJzgU74HmSMA7EIs+wfVkX+5sxy8tfl4n0qDGDmN95EhBYmFLGK/h4 -X3ljIWVWghjVaxSWZVLwDCe1wa4qFQS+8bQM4cy7t0l1pXRX2LTd7m9wqeDFo4pN -z5Dqw30LVJvR7wnoCriHMpbaZiHRhohjqdfyeY/S1mZQlaPVCJZQaBuGC4QNlDcl -Qskfvg8o2AqLmmHFMe+Lc638dvCaTWzmybftcsqwrgwAwKVkg1pmemFZ/55c ------END CERTIFICATE----- diff --git a/tests/certs/oadr310/vtn/key.pem b/tests/certs/oadr310/vtn/key.pem deleted file mode 100644 index d3ca6eb..0000000 --- a/tests/certs/oadr310/vtn/key.pem +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQDNt5uZzG/FOzJh -5VKMzSEhkB/zh/9CsK+5hTczQY0tUxS81GnhUaZWL14Jr+bPRr3FifJe6M/cuVLY -7hI3rpPIxPfE/0XrFJOYi/4rYHQZl5NcIKOJV01aWb6uVY6L4j1OHhcKkwINRMg9 -rWbEncpZqX/tLCgVuEaRiK6pUfqCMSq+2ZM1qaw9fFz45rIykBvFlzU9BxgCzptg -6Bfq6obqJlvRr274MwSu1LS3cBr0xOBm1XdmzEF6uWs6rZ8KYnPDo0NAeMDD7fB+ -663ThbQEOGZW5Eti1Of8cYM1ZaHYeKnqlGqkCnV4Q5/qElk5BJWau28emdCgwxCk -/Yncy4wctPEduHnH3eyCRhG10bUaucFm7EEQMjKPaY6CZ/xtm6tcjZ3cXLKpGCK4 -ooV+f2JcCsim33KIZAjI4dVg+fJFJArkwtqvfFYrRB5pPlKdYwEC64q97gv6/aVi -v3hBFeowQe5gcJp9mEn/6uNvirKze0xhLWQO7GjbNxyFTtKuTi7KudM1dKclrKlN -WyqXh8a0SaAle+2aNlLcQubpLBPfz82qc5H6PO16ZO2LXqLfvmYgu96C/kxQZEBu -IvhKj42sGSfsByx8+hFh+d9h0kVa0P6nk/abAH4ol8ZHxnVSJVCEStmjQsjV4/9y -mvVw2Pza9aFsLRWcuRegMWk3B9PeoQIDAQABAoICAF2GYvx6IDp33kD5hB01SSG8 -J3qNUrScJVffw6fO3oNDqULsAzMH7upx4Hvg97s2eyJqw8D4cT/1FUBAhBHCkbeO -LOLn936XvT52LtjDoiTcb4dL422WPjmcYX9asFLSSXLRZUJOcXlc0viPdBBheFRp -JhrKn3K4c4TIsjeSaKR1++wGeNHF0OOnPZF/OHaaY7XiN7cGGZl1Gtjky7Lmau3b -AJWE/5thfniJhZt8IRU0yJyX4g/fPsmvsJtdwddzyROhjvqpq2GoNFVPipiGyb+9 -cHGCmu9Uwh9X8AH/saP7v6xBS3ZGtv81J2fAbcXpWqadHji4T0w3QsLv7ZRvCcRV -a70rhnriPq/VP71fMq3KCfYqlPbL/qXCrvn+pMl0qmbtixnyNIbhERQy8VMJljlB -YYg2D4NHmpRDzCYeCr3C28KOtJS7og8O1LPUiXqS/P+avrKv6Hp3HwC1kufuJsQz -KCrZOe6CipnhePfWpR5fDRABSzaRQ1txCYyKJvijZU1l4XVQHMDhUW/zz6e74q4+ -eC19TuCqGEj6MQ0AhB7x82yHOtrCjWNqGJCCTn3uTIU5Bz4Kqi6HIcnZrnz4bcDg -24ydTDSBH/EtzwqvX4thjsPj4TMRb9p6K2U1Um3huJsGJFfSo+b4hgxfH3kvfZfo -Wws0U/KLLr02GHnj+nFNAoIBAQD/4uJPnkEqos5HzkmZ9WrVQaJ9aCvn74IsjBVb -ymp5bWG0rINmnuCaF1JVxFGcafayDWF8MXU8F3g1/s8YTI5zkYvLmOcF+GEEOLYg -L5YN+cNuHmtWdGIE89VPAc3B9gJ/IYIhywwchdv7S85o09qZ1zgYVxHNvwlYDn1k -1/bkDDh80K5f1Mqf/8HLu613nMmT1Q8cbd2O83BpyhYQKHqcI6shg/qb3UyMYjbr -xl+J5J+8xN5yzNagRoRLg7KCozL81Z6GfwzR5PiTHWQLG5GMrgfGrBvandgowQRh -ST++NGTTmhxFsBuJ5Duo7eWS1x/XnO+UmXOTC654JMAOkMJ/AoIBAQDNzwPrfnZT -hk+LTjNN+djEcb5LKdEluYePeDeS8j/z9T48PaQCl4HRU9zV40KZdo3jJIKKceRU -EGKVGZ1Dpj9+zrkdjO8U7zIlQ5RLxm5jxHCUGE5O4FN05513lIkXUoZlqv1qkIR1 -j8+qHBcfIUffDFQVFU6aRxu71gpJt1SyimzRgecv4tXURRxD8DYZHPNIoLQEBl/g -i23awxQ9kJ3MCD969UeBt9g0bYKhzLcVlEgYYe+FGE8uP57w0yk2uihJ4aX8Hvdt -YOpEXKcJp7Rpv80rxLOoNasWkQkQ9L720PQLNWf66HGo3F+RMJpPdotWWyH2q3Gh -R6oGRaRYKI7fAoIBAE8mrzpRiXbWxVgQx1OIRnOQyeJgxrQHsgx1iGXMRwMogoCQ -nFSx8Ge5rw44m8X2QQjyeqUf51OfLuAE4apyg93bt5JSyaNfISNdJ3jtP00zZPA7 -GJi1h1xg0CqQNEU7A6qvgsFRPX3VKiahXEewwH21xPRCeo9Z+Fn0oXUAGK6R2SdC -pa2e9M5pHlkWxKGrLe88iGAS2+XMB4mxqQF2g/aoUdccK/jqizMhbjugMQ1GWuLZ -QrXgBsOtDF8PbbhbIvpyWYbg5eZHHVzkSn3lGTCa5Gzf7/2/tY1lpyUwszbynf2W -PgiQYPRWVN38OHcjdAYN1U3x6X4iruUa+Q2IWaECggEABq+P8AmGrIXzdZIPxpu0 -OAV96hEssy4Iwldkbsih3Tn9UJWsC02CsRspVGVyl2urhlJ8TyUOBtEVcsSHQFhZ -PbRXfSdD6QkwNtewLJoAogsSoSz89kEBVQ3lJqpTncdXiFv6qYiSVQNb+qm48pDx -NcS/woOTrx1aQErewLBM9xPaHyA6M2sKCUBJf8eA6mXvXU+jKXJPkQrWgcHOmYVm -KuelsEfhj8w6INHh6YGt/Ox5PxmHX2p/v//fGPGzKAz6HBRRgRQ8c3gvhBMCdiTV -jo3li2NzdqZvrWb+xWc+AOSe2PJeM19zucmW7S+IA8bVFfQV8V52bYfXdSa0BQ0k -PwKCAQBQl+jihs2RH3vJhfGjmOLf9BcOsGvUuTwqBuzTKr+39siNLZLDfGE05iuB -ozgy1V7eFmh76olvcybnk7/Flts5lwN6IIvXdN4m8Y6vf0vcI644QYLP9P8bubNM -mVn6/fLR007OvV/Qv0iKmaVIBSWhT/7TOHoiOGPuqNSHRuGb8MzBRC6br0oqInYk -o+fJrruBmEKPIUGvwClADILRCjvjRoY8H0nJt8x0mJ2M4Jxx6A5BpWXrlQylny7i -Fxkg3p39HmInvUxvcV/+xCrwzD0Gb8duCdCI6C460u+S7zJ/0VFj92Y5OvV1+/Tr -Z9ZGqHnUh/jGln0zWKakdt9dWQdH ------END PRIVATE KEY----- From 62b410ce87bda927cef288dfabe75b55c242a085 Mon Sep 17 00:00:00 2001 From: Nick van der Burgt Date: Thu, 25 Jun 2026 13:43:22 +0200 Subject: [PATCH 09/13] Clean up str based time storage in favor of python native time type --- flexmeasures_openadr3/models/forms.py | 9 +++++-- flexmeasures_openadr3/models/storage.py | 8 +++--- ...measures_oadr3_ven_sensor_config_form.html | 2 +- flexmeasures_openadr3/utils/ven_clients.py | 26 +++---------------- flexmeasures_openadr3/utils/ven_jobs.py | 12 ++++----- 5 files changed, 21 insertions(+), 36 deletions(-) diff --git a/flexmeasures_openadr3/models/forms.py b/flexmeasures_openadr3/models/forms.py index 59f994a..ae20f5b 100644 --- a/flexmeasures_openadr3/models/forms.py +++ b/flexmeasures_openadr3/models/forms.py @@ -1,6 +1,7 @@ from __future__ import annotations from dataclasses import dataclass, field, replace +from datetime import time from typing import TYPE_CHECKING, TypeVar if TYPE_CHECKING: @@ -95,7 +96,7 @@ class VenSensorConfigFormValues: name: str = "" targets: str = "" - utc_trigger_time: str = "" + utc_trigger_time: time | None = None fetch_import_capacity_limits: str = "" fetch_export_capacity_limits: str = "" @@ -107,10 +108,14 @@ def empty(cls) -> VenSensorConfigFormValues: @classmethod def from_post_values(cls, post_values: VenSensorConfigPostValues) -> VenSensorConfigFormValues: """Build display form values from validated POST data.""" + try: + parsed_time: time | None = time.fromisoformat(post_values.utc_trigger_time) + except (ValueError, AttributeError): + parsed_time = None return cls( name=post_values.name, targets=post_values.targets, - utc_trigger_time=post_values.utc_trigger_time, + utc_trigger_time=parsed_time, fetch_import_capacity_limits="on" if post_values.fetch_import_capacity_limits else "", fetch_export_capacity_limits="on" if post_values.fetch_export_capacity_limits else "", ) diff --git a/flexmeasures_openadr3/models/storage.py b/flexmeasures_openadr3/models/storage.py index dd7a0c1..8f7d686 100644 --- a/flexmeasures_openadr3/models/storage.py +++ b/flexmeasures_openadr3/models/storage.py @@ -2,6 +2,7 @@ from collections.abc import Mapping from dataclasses import dataclass +from datetime import time VEN_CLIENT_ATTRIBUTE_KEY = "ven_client" @@ -12,7 +13,7 @@ class VenSensorConfigRecord: name: str targets: tuple[str, ...] = () - utc_trigger_time: str = "" + utc_trigger_time: time | None = None fetch_import_capacity_limits: bool = False fetch_export_capacity_limits: bool = False fetch_events_job_id: str | None = None @@ -25,10 +26,11 @@ def from_json(cls, data: Mapping[str, object]) -> VenSensorConfigRecord: targets = tuple(str(item) for item in raw_targets) if isinstance(raw_targets, list) else () job_id = data.get("fetch_events_job_id") + raw_time = str(data.get("utc_trigger_time", "") or "") return cls( name=str(data.get("name", "")), targets=targets, - utc_trigger_time=str(data.get("utc_trigger_time", "") or ""), + utc_trigger_time=time.fromisoformat(raw_time) if raw_time else None, fetch_import_capacity_limits=bool(data.get("fetch_import_capacity_limits", False)), fetch_export_capacity_limits=bool(data.get("fetch_export_capacity_limits", False)), fetch_events_job_id=str(job_id) if job_id is not None else None, @@ -39,7 +41,7 @@ def to_json(self) -> dict[str, object]: payload: dict[str, object] = { "name": self.name, "targets": list(self.targets), - "utc_trigger_time": self.utc_trigger_time, + "utc_trigger_time": self.utc_trigger_time.isoformat() if self.utc_trigger_time is not None else "", "fetch_import_capacity_limits": self.fetch_import_capacity_limits, "fetch_export_capacity_limits": self.fetch_export_capacity_limits, } diff --git a/flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_ven_sensor_config_form.html b/flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_ven_sensor_config_form.html index 4fc7284..1e5c9d4 100644 --- a/flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_ven_sensor_config_form.html +++ b/flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_ven_sensor_config_form.html @@ -65,7 +65,7 @@

{{ title }}

- + {% if form_errors.utc_trigger_time %}
{{ form_errors.utc_trigger_time }}
{% endif %} diff --git a/flexmeasures_openadr3/utils/ven_clients.py b/flexmeasures_openadr3/utils/ven_clients.py index 3a0be99..efc7cf0 100644 --- a/flexmeasures_openadr3/utils/ven_clients.py +++ b/flexmeasures_openadr3/utils/ven_clients.py @@ -1,6 +1,7 @@ from __future__ import annotations from dataclasses import dataclass, field +from datetime import time from flask import current_app from flexmeasures.data import db @@ -10,7 +11,6 @@ from openadr3_client.ven.http_factory import VirtualEndNodeHttpClientFactory from openadr3_client.version import OADRVersion from pydantic import BaseModel, Field, ValidationError, field_validator -from pydantic_core.core_schema import ValidationInfo from flexmeasures_openadr3.models.forms import ( FormValidationErrors, @@ -27,12 +27,6 @@ from flexmeasures_openadr3.utils.encryption import SecretsEncryptor from flexmeasures_openadr3.utils.sensor import VenAssetRepository, VenSensorRepository -_TIME_COMPONENT_COUNT = 3 -_MAX_HOUR = 23 -_MAX_MINUTE = 59 -_MAX_SECOND = 59 - - def _parse_csv_values(raw_value: str) -> list[str]: """Parse a comma-separated string into a list of trimmed, non-empty values.""" return [item.strip() for item in raw_value.split(",") if item.strip()] @@ -53,7 +47,7 @@ class VenSensorConfig: name: str targets: list[str] = field(default_factory=list) - utc_trigger_time: str = "" + utc_trigger_time: time | None = None fetch_import_capacity_limits: bool = False fetch_export_capacity_limits: bool = False fetch_events_job_id: str | None = None @@ -376,7 +370,7 @@ class VenSensorConfigFormData(BaseModel): name: str targets: list[str] = Field(default_factory=list) - utc_trigger_time: str + utc_trigger_time: time fetch_import_capacity_limits: bool = False fetch_export_capacity_limits: bool = False @@ -391,20 +385,6 @@ def _parse_targets_csv(cls, value: str | list[str] | tuple[str, ...] | None) -> return [item.strip() for item in value if item.strip()] return [item.strip() for item in value if item.strip()] - @field_validator("utc_trigger_time") - @classmethod - def _validate_utc_trigger_time(cls, value: str, _info: ValidationInfo) -> str: - parsed_value = value.strip() - time_parts = parsed_value.split(":") - if len(time_parts) != _TIME_COMPONENT_COUNT or not all(part.isdigit() for part in time_parts): - msg = "Must be in UTC format HH:MM:SS." - raise ValueError(msg) - hour, minute, second = (int(part) for part in time_parts) - if hour > _MAX_HOUR or minute > _MAX_MINUTE or second > _MAX_SECOND: - msg = "Must be in UTC format HH:MM:SS." - raise ValueError(msg) - return f"{hour:02d}:{minute:02d}:{second:02d}" - @classmethod def from_post_values(cls, post_values: VenSensorConfigPostValues) -> VenSensorConfigFormData: """Build validated data from parsed polling schedule POST values.""" diff --git a/flexmeasures_openadr3/utils/ven_jobs.py b/flexmeasures_openadr3/utils/ven_jobs.py index 4ad5788..47a31ef 100644 --- a/flexmeasures_openadr3/utils/ven_jobs.py +++ b/flexmeasures_openadr3/utils/ven_jobs.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import UTC, datetime, timedelta +from datetime import UTC, datetime, time, timedelta from typing import TYPE_CHECKING, cast from flask import current_app @@ -69,10 +69,9 @@ def _build_job_id(ven_id: int, config_name: str, run_at: datetime) -> str: return f"oadr3-ven-{ven_id}-{config_name}-fetch-events-{int(run_at.timestamp())}" -def _next_daily_trigger_datetime(utc_trigger_time: str) -> datetime: - hour, minute, second = (int(part) for part in utc_trigger_time.split(":")) +def _next_daily_trigger_datetime(utc_trigger_time: time) -> datetime: now_utc = datetime.now(UTC) - trigger = now_utc.replace(hour=hour, minute=minute, second=second, microsecond=0) + trigger = now_utc.replace(hour=utc_trigger_time.hour, minute=utc_trigger_time.minute, second=utc_trigger_time.second, microsecond=0) if trigger <= now_utc: trigger += timedelta(days=1) return trigger @@ -201,14 +200,13 @@ def schedule( replace_existing: bool = True, ) -> Job | None: """Schedule or reschedule the daily fetch-events job for a sensor config.""" - utc_trigger_time = sensor_config.utc_trigger_time.strip() - if not utc_trigger_time: + if sensor_config.utc_trigger_time is None: return None if replace_existing and sensor_config.fetch_events_job_id: _delete_job(sensor_config.fetch_events_job_id) - run_at = _next_daily_trigger_datetime(utc_trigger_time) + run_at = _next_daily_trigger_datetime(sensor_config.utc_trigger_time) job_id = _build_job_id(ven_client.id, sensor_config.name, run_at) queue = _fetch_events_queue() From 5cbc0c032a838b6505b5d31fcbc5ca0980d1e000 Mon Sep 17 00:00:00 2001 From: Nick van der Burgt Date: Fri, 26 Jun 2026 13:34:13 +0200 Subject: [PATCH 10/13] Fix structure of tests. Ensure tests pass with FM 0.33.0 --- .github/workflows/lint-and-test.yml | 38 ++++++++++++++----- Dockerfile | 2 - docker-compose.yml | 2 +- .../README.md | 2 +- .../docker-compose.yml | 2 +- flexmeasures_openadr3/__init__.py | 4 +- ...measures_oadr3_ven_sensor_config_form.html | 2 +- flexmeasures_openadr3/utils/ven_jobs.py | 2 +- tests/test_ven_client_repository.py | 3 ++ tests/test_ven_fetch_e2e.py | 1 + tests/test_ven_fetch_jobs.py | 6 ++- 11 files changed, 45 insertions(+), 19 deletions(-) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index 0032d65..73239e7 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -11,19 +11,39 @@ jobs: - uses: actions/setup-python@d09bd5e6005b175076f227b13d9730d56e9dcfcb with: python-version: 3.11 + - name: Install uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + version: "0.9.22" + enable-cache: true - uses: actions/checkout@v3 - uses: pre-commit/action@646c83fcd040023954eafda54b4db0192ce70507 - test: - needs: check + tests: runs-on: ubuntu-latest strategy: - fail-fast: false - name: "Test (on Python 3.11)" + matrix: + python-version: ["3.11"] steps: - - uses: actions/setup-python@d09bd5e6005b175076f227b13d9730d56e9dcfcb + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - python-version: 3.11 - - name: Check out src from Git - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 - - run: make test + persist-credentials: false + + - name: Install uv and set Python version + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + version: "0.9.22" + enable-cache: true + python-version: ${{ matrix.python-version }} + + - id: install_dependencies + run: uv sync --locked --all-extras --dev + + - id: run_tests + run: uv run pytest --cov . --cov-report xml + + - name: Upload test coverage xml as artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: coverage-results-${{ matrix.python-version }} + path: coverage.xml diff --git a/Dockerfile b/Dockerfile index cd99a5b..aa1c06a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,5 @@ FROM lfenergy/flexmeasures:v0.33.0 -# FROM flexmeasures-local:latest - COPY --from=ghcr.io/astral-sh/uv:0.10-python3.12-trixie-slim /usr/local/bin/uv /usr/local/bin/uv COPY . /app/plugins/flexmeasures-openadr3 diff --git a/docker-compose.yml b/docker-compose.yml index 170ccb7..29c84f3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -127,7 +127,7 @@ services: if [ -f /usr/var/flexmeasures-instance/requirements.txt ]; then pip install --no-cache-dir -r /usr/var/flexmeasures-instance/requirements.txt fi - flexmeasures jobs run-worker --name flexmeasures-worker --queue forecasting\|scheduling + flexmeasures jobs run-worker --name flexmeasures-worker --queue ingestion test-db: image: postgres expose: diff --git a/examples/capacity-limits-ingestion-walkthrough/README.md b/examples/capacity-limits-ingestion-walkthrough/README.md index f31cbe6..b2e3f45 100644 --- a/examples/capacity-limits-ingestion-walkthrough/README.md +++ b/examples/capacity-limits-ingestion-walkthrough/README.md @@ -179,7 +179,7 @@ Fetched DR events for VEN 'demo-ven' … stored 192 beliefs. | Seed script cannot reach VTN | `docker compose ps` — is `openleadr-vtn` running? Wait ~30s after startup. | | OAuth errors in seed script | Keycloak at `http://localhost:8080` — realm imported? | | VEN client save fails | URLs must use Docker service names (`openleadr-vtn`, `keycloak`), not `localhost`. | -| Fetch job never runs | `docker compose ps worker` — worker must be up on the `forecasting` queue. | +| Fetch job never runs | `docker compose ps worker` — worker must be up on the `ingestion` queue. | | No beliefs after fetch | Re-run `seed_events.py`; event must exist on VTN before fetch. | | HTTP blocked to VTN | `ALLOW_INSECURE_HTTP_VTN=true` is set in `docker-compose.yml`. | diff --git a/examples/capacity-limits-ingestion-walkthrough/docker-compose.yml b/examples/capacity-limits-ingestion-walkthrough/docker-compose.yml index 0604474..b3acb2a 100644 --- a/examples/capacity-limits-ingestion-walkthrough/docker-compose.yml +++ b/examples/capacity-limits-ingestion-walkthrough/docker-compose.yml @@ -166,7 +166,7 @@ services: - ./python:/walkthrough/python:ro entrypoint: ["/bin/sh", "-c"] command: - - flexmeasures jobs run-worker --name flexmeasures-worker --queue forecasting\|scheduling + - flexmeasures jobs run-worker --name flexmeasures-worker --queue ingestion networks: - oadr3-walkthrough diff --git a/flexmeasures_openadr3/__init__.py b/flexmeasures_openadr3/__init__.py index f2c7f2e..64fe29b 100644 --- a/flexmeasures_openadr3/__init__.py +++ b/flexmeasures_openadr3/__init__.py @@ -23,8 +23,8 @@ try: _fm_version = pkg_version("flexmeasures") - if Version(_fm_version) < Version("v0.32.0"): - warnings.warn(f"flexmeasures-openadr3 requires FlexMeasures >= v0.32.0, but version {_fm_version} is installed.", stacklevel=2) + if Version(_fm_version) < Version("v0.33.0"): + warnings.warn(f"flexmeasures-openadr3 requires FlexMeasures >= v0.33.0, but version {_fm_version} is installed.", stacklevel=2) except PackageNotFoundError: pass diff --git a/flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_ven_sensor_config_form.html b/flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_ven_sensor_config_form.html index 1e5c9d4..354ae5d 100644 --- a/flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_ven_sensor_config_form.html +++ b/flexmeasures_openadr3/ui/templates/flexmeasures_oadr3_ven_sensor_config_form.html @@ -65,7 +65,7 @@

{{ title }}

- + {% if form_errors.utc_trigger_time %}
{{ form_errors.utc_trigger_time }}
{% endif %} diff --git a/flexmeasures_openadr3/utils/ven_jobs.py b/flexmeasures_openadr3/utils/ven_jobs.py index 47a31ef..ac66560 100644 --- a/flexmeasures_openadr3/utils/ven_jobs.py +++ b/flexmeasures_openadr3/utils/ven_jobs.py @@ -279,4 +279,4 @@ def _execute(self, ven_id: int, config_name: str) -> None: db.session.commit() finally: self.schedule(ven_client, sensor_config, replace_existing=False) - db.session.commit() + db.session.commit() \ No newline at end of file diff --git a/tests/test_ven_client_repository.py b/tests/test_ven_client_repository.py index 678264d..9d6a970 100644 --- a/tests/test_ven_client_repository.py +++ b/tests/test_ven_client_repository.py @@ -25,6 +25,7 @@ def test_create_ven_client_persists_connection_attributes( fresh_db: SQLAlchemy, + logged_in_prosumer: object, # noqa: ARG001 ven_client_repository: VenClientRepository, sample_ven_client_form_data: VenClientFormData, ) -> None: @@ -46,6 +47,7 @@ def test_create_ven_client_persists_connection_attributes( def test_create_ven_client_encrypts_oauth_credentials_in_attributes( fresh_db: SQLAlchemy, + logged_in_prosumer: object, # noqa: ARG001 ven_client_repository: VenClientRepository, sample_ven_client_form_data: VenClientFormData, ) -> None: @@ -157,6 +159,7 @@ def test_delete_sensor_config_removes_schedule_from_attributes( def test_duplicate_ven_client_name_raises( + created_ven_client: VenClient, # noqa: ARG001 ven_client_repository: VenClientRepository, sample_ven_client_form_data: VenClientFormData, ) -> None: diff --git a/tests/test_ven_fetch_e2e.py b/tests/test_ven_fetch_e2e.py index 4c9842e..d3b6e73 100644 --- a/tests/test_ven_fetch_e2e.py +++ b/tests/test_ven_fetch_e2e.py @@ -60,6 +60,7 @@ def _beliefs_for_sensor(db: SQLAlchemy, sensor_id: int) -> list[TimedBelief]: def test_ven_fetch_events_e2e_stores_openadr_capacity_limits( app: Flask, + logged_in_prosumer: object, # noqa: ARG001 fresh_db: SQLAlchemy, ven_client: IntegrationTestVTNClient, vtn_capacity_limit_event_seed: ExistingEvent, diff --git a/tests/test_ven_fetch_jobs.py b/tests/test_ven_fetch_jobs.py index e13119a..7261c53 100644 --- a/tests/test_ven_fetch_jobs.py +++ b/tests/test_ven_fetch_jobs.py @@ -33,6 +33,8 @@ def test_schedule_enqueues_job_in_forecasting_queue( config = reloaded.get_sensor_config("daily-poll") assert config is not None + assert FETCH_EVENTS_QUEUE_NAME in app.queues + job = ven_fetch_job_scheduler.schedule(reloaded, config) fresh_db.session.commit() @@ -83,7 +85,7 @@ def test_schedule_without_utc_trigger_time_returns_none( config = VenSensorConfig( name="no-trigger", targets=["t1"], - utc_trigger_time="", + utc_trigger_time=None, fetch_import_capacity_limits=True, ) created_ven_client.sensor_configs.append(config) @@ -107,6 +109,7 @@ def test_delete_removes_scheduled_job_from_queue( config = reloaded.get_sensor_config("daily-poll") assert config is not None + assert FETCH_EVENTS_QUEUE_NAME in app.queues job = ven_fetch_job_scheduler.schedule(reloaded, config) fresh_db.session.commit() @@ -133,6 +136,7 @@ def test_delete_all_removes_jobs_for_every_schedule( """delete_all clears scheduled jobs for all polling schedules on a VEN client.""" reloaded = ven_client_repository.find_by_id(created_ven_client_with_schedule.id) assert reloaded is not None + assert FETCH_EVENTS_QUEUE_NAME in app.queues for config in reloaded.sensor_configs: ven_fetch_job_scheduler.schedule(reloaded, config) From a5df2a424d49054c3508ddb4c6920575c4d65da5 Mon Sep 17 00:00:00 2001 From: Nick van der Burgt Date: Fri, 26 Jun 2026 13:39:39 +0200 Subject: [PATCH 11/13] Fix type errors, formatting --- .github/workflows/lint-and-test.yml | 4 ++-- flexmeasures_openadr3/ui/views/dashboard.py | 2 +- flexmeasures_openadr3/utils/ven_clients.py | 3 ++- flexmeasures_openadr3/utils/ven_jobs.py | 2 +- tests/conftest.py | 3 ++- tests/test_ven_fetch_e2e.py | 4 +++- 6 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index 73239e7..9e44dc4 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/setup-python@d09bd5e6005b175076f227b13d9730d56e9dcfcb with: - python-version: 3.11 + python-version: 3.12 - name: Install uv uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.11"] + python-version: ["3.12"] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: diff --git a/flexmeasures_openadr3/ui/views/dashboard.py b/flexmeasures_openadr3/ui/views/dashboard.py index 3014618..4c39bc7 100644 --- a/flexmeasures_openadr3/ui/views/dashboard.py +++ b/flexmeasures_openadr3/ui/views/dashboard.py @@ -121,7 +121,7 @@ def sensor_configs_overview(ven_id: int) -> str: ven_name=ven_client.name, config_name=cfg.name, targets=tuple(cfg.targets), - utc_trigger_time=cfg.utc_trigger_time, + utc_trigger_time=cfg.utc_trigger_time.isoformat() if cfg.utc_trigger_time else "", fetch_import_capacity_limits=cfg.fetch_import_capacity_limits, fetch_export_capacity_limits=cfg.fetch_export_capacity_limits, import_sensor_id=cfg.import_sensor.id if cfg.import_sensor else None, diff --git a/flexmeasures_openadr3/utils/ven_clients.py b/flexmeasures_openadr3/utils/ven_clients.py index efc7cf0..bdfd0c9 100644 --- a/flexmeasures_openadr3/utils/ven_clients.py +++ b/flexmeasures_openadr3/utils/ven_clients.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from datetime import time +from datetime import time # NOQA: TC003 from flask import current_app from flexmeasures.data import db @@ -27,6 +27,7 @@ from flexmeasures_openadr3.utils.encryption import SecretsEncryptor from flexmeasures_openadr3.utils.sensor import VenAssetRepository, VenSensorRepository + def _parse_csv_values(raw_value: str) -> list[str]: """Parse a comma-separated string into a list of trimmed, non-empty values.""" return [item.strip() for item in raw_value.split(",") if item.strip()] diff --git a/flexmeasures_openadr3/utils/ven_jobs.py b/flexmeasures_openadr3/utils/ven_jobs.py index ac66560..47a31ef 100644 --- a/flexmeasures_openadr3/utils/ven_jobs.py +++ b/flexmeasures_openadr3/utils/ven_jobs.py @@ -279,4 +279,4 @@ def _execute(self, ven_id: int, config_name: str) -> None: db.session.commit() finally: self.schedule(ven_client, sensor_config, replace_existing=False) - db.session.commit() \ No newline at end of file + db.session.commit() diff --git a/tests/conftest.py b/tests/conftest.py index e4b778b..b12be92 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ from __future__ import annotations import os +from datetime import time from pathlib import Path from typing import TYPE_CHECKING @@ -116,7 +117,7 @@ def sample_sensor_config_form_data() -> VenSensorConfigFormData: return VenSensorConfigFormData( name="daily-poll", targets=["target-a", "target-b"], - utc_trigger_time="02:30:00", + utc_trigger_time=time.fromisoformat("02:30:00"), fetch_import_capacity_limits=True, fetch_export_capacity_limits=True, ) diff --git a/tests/test_ven_fetch_e2e.py b/tests/test_ven_fetch_e2e.py index d3b6e73..b616b67 100644 --- a/tests/test_ven_fetch_e2e.py +++ b/tests/test_ven_fetch_e2e.py @@ -2,6 +2,8 @@ from __future__ import annotations +from datetime import time + import pytest from flask import Flask from flask_sqlalchemy import SQLAlchemy @@ -85,7 +87,7 @@ def test_ven_fetch_events_e2e_stores_openadr_capacity_limits( schedule_form = VenSensorConfigFormData( name="e2e-poll", targets=[], - utc_trigger_time="00:00:00", + utc_trigger_time=time.fromisoformat("00:00:00"), fetch_import_capacity_limits=True, fetch_export_capacity_limits=True, ) From 6629c7c02ae435cf45640a24ce73dc84e75a1d6e Mon Sep 17 00:00:00 2001 From: Nick van der Burgt Date: Fri, 26 Jun 2026 13:45:16 +0200 Subject: [PATCH 12/13] Newline fix --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3e1dcf3..5c21668 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,4 +123,4 @@ ignore = [ # Ignores S101 ("Use of 'assert' detected") for the tests. [tool.ruff.lint.per-file-ignores] "tests/*" = ["S101", "ANN201", "SLF001", "S106", "S105", "S311", "PLR2004", "PT018"] -"examples/**" = ["EXE001", "T201", "S311", "S105", "INP001", "SLF001"] \ No newline at end of file +"examples/**" = ["EXE001", "T201", "S311", "S105", "INP001", "SLF001"] From a0f6cd0ffbafc50998474cd78e72ad056c4b24f6 Mon Sep 17 00:00:00 2001 From: Nick van der Burgt Date: Fri, 26 Jun 2026 13:49:35 +0200 Subject: [PATCH 13/13] newline fix gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 322ba95..8345097 100644 --- a/.gitignore +++ b/.gitignore @@ -88,4 +88,4 @@ docker-compose-data ci flexmeasures-instance -.cursor \ No newline at end of file +.cursor