diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7a2b713..1ba4dc7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,8 +19,14 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v6 + - name: Install PostgreSQL and PostGIS tooling + run: | + sudo apt-get update + sudo apt-get install -y postgresql-16 postgresql-client-16 postgresql-16-postgis-3 postgresql-16-postgis-3-scripts + echo "/usr/lib/postgresql/16/bin" >> "$GITHUB_PATH" + - name: Install the project run: uv sync --all-extras --dev - name: Run tests - run: uv run pytest tests/ \ No newline at end of file + run: uv run pytest tests/ diff --git a/.gitignore b/.gitignore index 41e29a2..8d8939a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# OpenSAMPL data paths +archive/ +ntp-snapshots/ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/CHANGELOG.md b/CHANGELOG.md index dfb7252..5803775 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,13 +37,27 @@ This project adheres to [Semantic Versioning](https://semver.org/). *Unreleased* versions radiate potential—-and dread. Once you merge an infernal PR, move its bullet under a new version heading with the actual release date.* --> -## [Unreleased] - YYYY-MM-DD +## [1.2.0] - Unreleased ### Added +- 🔥 First-class NTP vendor and probe support using the existing OpenSAMPL extension model +- 🔥 Local and remote NTP collection paths, including `ntp_metadata` loading behavior +- 🔥 NTP-focused metrics such as jitter, delay, stratum, reachability, root delay, root dispersion, poll interval, and sync health +- 🔥 Additional NTP metadata handling for collector/target probe relationships and reference-backed loading +- 🔥 Compact reference/source metadata views in dashboards to improve interpretation of NTP-backed timing data +- 🔥 Documentation covering the NTP extension path, collection semantics, and geolocation behavior +- 🔥 Additional unit and integration-style tests for NTP collection, loading, geolocation helpers, and seeded database defaults - 🔥 Moved alembic migration code into openSAMPL along with Docker image information - 🔥 Moved backend api code into openSAMPL along with Docker image information - 🔥 Docker-compose for developers which installs openSAMPL as editable on backend image +### Changed +- ⚡ Hardened dashboard queries and variables to avoid brittle empty-filter handling and varchar-versus-UUID failures +- ⚡ Updated timing dashboards and wording to use reference-safe terminology for NTP-backed demo paths +- ⚡ Reworked integration-style tests to use the project MockDB harness instead of requiring a locally spawned PostgreSQL instance +- ⚡ Updated CI to install PostgreSQL/PostGIS tooling so the workflow can support `pytest-postgresql`-style environments when needed + ### Fixed +- 🩹 Seeded default metric UUID handling in the MockDB test harness now points to the UNKNOWN metric as intended - 🩹 Bug which caused random data duration to always be 1 hour ## [1.1.5] - 2025-09-22 diff --git a/README.md b/README.md index f67f9ae..ce8d3a5 100644 --- a/README.md +++ b/README.md @@ -20,143 +20,153 @@ -OpenSAMPL was created to provide a set of Python tools for managing clock data in a TimescaleDB database, specifically designed for synchronization analytics and monitoring. -This project came out of [**CAST**](https://cast.ornl.gov), the **C**enter for **A**lternative **S**yncrhonization and **T**iming, a research group at Oak Ridge National Laboratory (ORNL). -The name OpenSAMPL stands for **O**pen **S**ynchronization **A**nalytics and **M**onitoring **PL**atform, and provides the code and logic for uploading, managing, and visualizing clock data from various sources, including ADVA probes and Microchip TWST data files, -with the goal of this project being to provide a comprehensive and open-source solution for clock data management and analysis. -Visualizations are provided via [grafana](https://grafana.com/), and the data is stored in a [TimescaleDB](https://www.timescale.com/) database, which is a time-series database built on PostgreSQL. +OpenSAMPL provides Python tools for collecting, loading, and visualizing clock data in a +TimescaleDB-backed synchronization analytics stack. +This project came out of [**CAST**](https://cast.ornl.gov), the **C**enter for +**A**lternative **S**ynchronization and **T**iming at Oak Ridge National Laboratory (ORNL). +The name OpenSAMPL stands for **O**pen **S**ynchronization **A**nalytics and +**M**onitoring **PL**atform. + +The current codebase supports loading and analysis workflows for ADVA, Microchip TWST, +Microchip TP4100, and NTP-derived probe data. Visualization is provided through +[Grafana](https://grafana.com/), and the data is stored in +[TimescaleDB](https://www.timescale.com/), which is built on PostgreSQL. ### (**O**pen **S**ynchronization **A**nalytics and **M**onitoring **PL**atform) -python tools for adding clock data to a timescale db. +Python tools for adding clock and timing data to a TimescaleDB database. -## CLI TOOL +## Installation -### Installation +1. Ensure you have Python 3.10 or higher installed. +2. Install the latest release: -1. Ensure you have Python 3.9 or higher installed -2. Pip install the latest version of opensampl: ```bash pip install opensampl ``` ### Development Setup + ```bash uv venv -uv sync --extra all +uv sync --all-extras --dev source .venv/bin/activate ``` -This will create a virtual environment and install the development dependencies. +This creates a virtual environment and installs the development dependencies. ### Environment Setup -The tool requires several environment variables. Create a `.env` file in your project root: +The CLI reads configuration from environment variables or a local `.env` file. -When routing through a backend: +When routing through a backend service: ```bash -ROUTE_TO_BACKEND=true # Set to true if using backend service -BACKEND_URL=http://localhost:8000 # Only needed if ROUTE_TO_BACKEND is true +ROUTE_TO_BACKEND=true +BACKEND_URL=http://localhost:8000 -# Archive configuration -ARCHIVE_PATH=/path/to/archive # Where processed files are stored +ARCHIVE_PATH=/path/to/archive ``` -When directly accessing db: + +When connecting directly to PostgreSQL / TimescaleDB: ```bash -# Database connection DATABASE_URL=postgresql://:@:/ - -# Archive configuration -ARCHIVE_PATH=/path/to/archive # Where processed files are stored +ARCHIVE_PATH=/path/to/archive ``` -### Basic Usage +Use `opensampl config show` to inspect the current resolved configuration. -The CLI tool provides several commands. You can use `opensampl --help` (or, any deeper `opensampl [command] --help`) to get details +## CLI -#### Load Probe Data +The main CLI exposes `collect`, `config`, `create`, `init`, and `load`. +Use `opensampl --help` and `opensampl --help` for current options. -Load data from ADVA probes: +If you plan to use the NTP, Microchip TWST, or Microchip TP4100 collectors, install the optional collection dependencies: ```bash -# Load single file -opensampl load probe adva path/to/file.txt.gz - -# Load directory of files -opensampl load probe adva path/to/directory/ +pip install "opensampl[collect]" ``` -ADVA probes have all their metadata and their time data in each file, so no need to use the `-m` or `-t` options, though if you want to skip loading one or the other it becomes useful! -options: -- `--metadata` (`-m`): Only load probe metadata -- `--time-data` (`-t`): Only load time series data -- `--no-archive` (`-n`): Don't archive processed files -- `--archive-path` (`-a`): Override default archive directory -- `--max-workers` (`-w`): Maximum number of worker threads (default: 4) -- `--chunk-size` (`-c`): Number of time data entries per batch (default: 10000) +### Load Probe Data -#### Load Direct Table Data +Load data with the probe type name directly: -Load data directly into a database table. Format can be yaml or json. Can be a list of dictionaries or a single dictionary. +```bash +opensampl load ADVA path/to/file.txt.gz +opensampl load ADVA path/to/directory/ +``` -you do not have to specify schema, is assumed to be castdb. +ADVA files bundle metadata and time-series data in a single file, so the split flags are +usually not needed. -The --if-exists option controls how to handle conflicts: - - update: Only update fields that are provided and non-default (default) - - error: Raise an error if entry exists - - replace: Replace all non-primary-key fields with new values - - ignore: Skip if entry exists +```bash +opensampl load MicrochipTWST path/to/twst-output +opensampl load MicrochipTP4100 path/to/tp4100-output +``` + +NTP data is collected first and then loaded from the output directory: ```bash -opensampl load table table_name path/to/data.yaml +opensampl collect ntp --mode remote --server pool.ntp.org --output-path ./ntp-out +opensampl load NTP ./ntp-out ``` -So, you can do things like the following +Load options: + +- `--metadata` / `-m`: load only probe metadata +- `--time-data` / `-t`: load only time-series data +- `--no-archive` / `-n`: skip archiving processed files +- `--archive-path` / `-a`: override the archive directory +- `--max-workers` / `-w`: set the worker count +- `--chunk-size` / `-c`: set the batch size for time-series inserts + +### Load Direct Table Data + +Load YAML or JSON directly into a table: + ```bash -opensampl load table locations --if-exists replace updated_location.yaml +opensampl load table locations updated_location.yaml ``` -Where this is the updated_location + +Conflict handling is controlled by `--if-exists`: + +- `update`: fill null fields in an existing row +- `error`: raise if the row exists +- `replace`: replace non-primary-key values +- `ignore`: skip existing rows + +Example input: + ```yaml name: EPB Chattanooga lat: 35.9311256 lon: -84.3292469 ``` -And it will overwrite the existing entry for EPB Chattanooga, or create a new one if it doesn't exist yet. - ### View Configuration -Display current environment configuration: - ```bash -# Show all variables -poetry run opensampl config show - -# Show with descriptions -poetry run opensampl config show --explain - -# Show specific variable -poetry run opensampl config show --var DATABASE_URL +opensampl config show +opensampl config show --explain +opensampl config show --var DATABASE_URL ``` ### Set Configuration -Update environment variables: - ```bash -poetry run opensampl config set VARIABLE_NAME value +opensampl config set VARIABLE_NAME value ``` ## File Format Support -The tool currently supports: - -ADVA probe data files with the following naming convention: -`CLOCK_PROBE--YYYY-MM-DD-HH-MM-SS.txt.gz` +The loaders currently support: -Example: `10.0.0.121CLOCK_PROBE-1-1-2024-01-02-18-24-56.txt.gz` +- ADVA probe files named like + `CLOCK_PROBE--YYYY-MM-DD-HH-MM-SS.txt.gz>` +- Microchip TWST and TP4100 output produced by the collector tooling +- NTP snapshot output produced by `opensampl collect ntp` -Microchip TWST Data Files as generated by the script available. +Example ADVA file: +`10.0.0.121CLOCK_PROBE-1-1-2024-01-02-18-24-56.txt.gz` # Contributing We welcome contributions! Please see our [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to get started. @@ -234,4 +244,3 @@ adva_mask_margin: 0 # Mask margin - Table relationships are maintained through UUID references - Geographic coordinates use WGS84 projection (SRID 4326) by default - Boolean fields (public) are optional and can be null - diff --git a/docs/api/collect/cli.md b/docs/api/collect/cli.md new file mode 100644 index 0000000..50133b9 --- /dev/null +++ b/docs/api/collect/cli.md @@ -0,0 +1,8 @@ +# CLI Reference + +This page provides documentation for our command line tools. + +::: mkdocs-click + :module: opensampl.collect.cli + :command: cli + :prog_name: cli \ No newline at end of file diff --git a/docs/api/collect/microchip/tp4100/collect_4100.md b/docs/api/collect/microchip/tp4100/collect_4100.md new file mode 100644 index 0000000..a038496 --- /dev/null +++ b/docs/api/collect/microchip/tp4100/collect_4100.md @@ -0,0 +1,7 @@ +# `opensampl.collect.microchip.tp4100.collect_4100` + +::: opensampl.collect.microchip.tp4100.collect_4100 + options: + show_root_heading: false + show_submodules: true + show_source: true diff --git a/docs/api/collect/microchip/twst/context.md b/docs/api/collect/microchip/twst/context.md new file mode 100644 index 0000000..259ba2e --- /dev/null +++ b/docs/api/collect/microchip/twst/context.md @@ -0,0 +1,7 @@ +# `opensampl.collect.microchip.twst.context` + +::: opensampl.collect.microchip.twst.context + options: + show_root_heading: false + show_submodules: true + show_source: true diff --git a/docs/api/collect/microchip/twst/generate_twst_files.md b/docs/api/collect/microchip/twst/generate_twst_files.md new file mode 100644 index 0000000..e4f4d19 --- /dev/null +++ b/docs/api/collect/microchip/twst/generate_twst_files.md @@ -0,0 +1,7 @@ +# `opensampl.collect.microchip.twst.generate_twst_files` + +::: opensampl.collect.microchip.twst.generate_twst_files + options: + show_root_heading: false + show_submodules: true + show_source: true diff --git a/docs/api/collect/microchip/twst/readings.md b/docs/api/collect/microchip/twst/readings.md new file mode 100644 index 0000000..d92238c --- /dev/null +++ b/docs/api/collect/microchip/twst/readings.md @@ -0,0 +1,7 @@ +# `opensampl.collect.microchip.twst.readings` + +::: opensampl.collect.microchip.twst.readings + options: + show_root_heading: false + show_submodules: true + show_source: true diff --git a/docs/api/ats6502/modem.md b/docs/api/collect/modem.md similarity index 63% rename from docs/api/ats6502/modem.md rename to docs/api/collect/modem.md index 383ac06..b7b3ca1 100644 --- a/docs/api/ats6502/modem.md +++ b/docs/api/collect/modem.md @@ -1,6 +1,6 @@ -# `opensampl.ats6502.modem` +# `opensampl.collect.modem` -::: opensampl.ats6502.modem +::: opensampl.collect.modem options: show_root_heading: false show_submodules: true diff --git a/docs/api/ats6502/context.md b/docs/api/config/server.md similarity index 61% rename from docs/api/ats6502/context.md rename to docs/api/config/server.md index 152da13..8e39fe5 100644 --- a/docs/api/ats6502/context.md +++ b/docs/api/config/server.md @@ -1,6 +1,6 @@ -# `opensampl.ats6502.context` +# `opensampl.config.server` -::: opensampl.ats6502.context +::: opensampl.config.server options: show_root_heading: false show_submodules: true diff --git a/docs/api/ats6502/readings.md b/docs/api/config/tp4100.md similarity index 61% rename from docs/api/ats6502/readings.md rename to docs/api/config/tp4100.md index 8d282af..73f4740 100644 --- a/docs/api/ats6502/readings.md +++ b/docs/api/config/tp4100.md @@ -1,6 +1,6 @@ -# `opensampl.ats6502.readings` +# `opensampl.config.tp4100` -::: opensampl.ats6502.readings +::: opensampl.config.tp4100 options: show_root_heading: false show_submodules: true diff --git a/docs/api/create/create_vendor.md b/docs/api/create/create_vendor.md new file mode 100644 index 0000000..a645c13 --- /dev/null +++ b/docs/api/create/create_vendor.md @@ -0,0 +1,7 @@ +# `opensampl.create.create_vendor` + +::: opensampl.create.create_vendor + options: + show_root_heading: false + show_submodules: true + show_source: true diff --git a/docs/api/helpers/create_vendor.md b/docs/api/create/insert_markers.md similarity index 57% rename from docs/api/helpers/create_vendor.md rename to docs/api/create/insert_markers.md index 35f5b08..6f5c0b8 100644 --- a/docs/api/helpers/create_vendor.md +++ b/docs/api/create/insert_markers.md @@ -1,6 +1,6 @@ -# `opensampl.helpers.create_vendor` +# `opensampl.create.insert_markers` -::: opensampl.helpers.create_vendor +::: opensampl.create.insert_markers options: show_root_heading: false show_submodules: true diff --git a/docs/api/helpers/geolocator.md b/docs/api/helpers/geolocator.md new file mode 100644 index 0000000..12509a3 --- /dev/null +++ b/docs/api/helpers/geolocator.md @@ -0,0 +1,7 @@ +# `opensampl.helpers.geolocator` + +::: opensampl.helpers.geolocator + options: + show_root_heading: false + show_submodules: true + show_source: true diff --git a/docs/api/helpers/insert_markers.md b/docs/api/helpers/insert_markers.md deleted file mode 100644 index 62a679d..0000000 --- a/docs/api/helpers/insert_markers.md +++ /dev/null @@ -1,7 +0,0 @@ -# `opensampl.helpers.insert_markers` - -::: opensampl.helpers.insert_markers - options: - show_root_heading: false - show_submodules: true - show_source: true diff --git a/docs/api/helpers/source_writer.md b/docs/api/helpers/source_writer.md deleted file mode 100644 index 37d6139..0000000 --- a/docs/api/helpers/source_writer.md +++ /dev/null @@ -1,7 +0,0 @@ -# `opensampl.helpers.source_writer` - -::: opensampl.helpers.source_writer - options: - show_root_heading: false - show_submodules: true - show_source: true diff --git a/docs/api/index.md b/docs/api/index.md index 8b77d05..e3bb38d 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -4,32 +4,49 @@ Welcome to the OpenSAMPL API documentation. Browse the modules and packages below: -- Ats6502 - - [Context](ats6502/context.md) - - [Modem](ats6502/modem.md) - - [Readings](ats6502/readings.md) - [Cli](cli.md) +- Collect + - [Cli](collect/cli.md) + - Microchip + - Tp4100 + - [Collect 4100](collect/microchip/tp4100/collect_4100.md) + - Twst + - [Context](collect/microchip/twst/context.md) + - [Generate Twst Files](collect/microchip/twst/generate_twst_files.md) + - [Readings](collect/microchip/twst/readings.md) + - [Modem](collect/modem.md) - Config - [Base](config/base.md) + - [Server](config/server.md) + - [Tp4100](config/tp4100.md) +- Create + - [Create Vendor](create/create_vendor.md) + - [Insert Markers](create/insert_markers.md) - Db - [Access Orm](db/access_orm.md) - [Orm](db/orm.md) - Helpers - - [Create Vendor](helpers/create_vendor.md) - - [Insert Markers](helpers/insert_markers.md) - - [Source Writer](helpers/source_writer.md) + - [Geolocator](helpers/geolocator.md) - Load - [Data](load/data.md) - - [Probe](load/probe.md) - [Routing](load/routing.md) - [Table Factory](load/table_factory.md) - [Load Data](load_data.md) - [Metrics](metrics.md) +- Mixins + - [Collect](mixins/collect.md) + - [Random Data](mixins/random_data.md) - [References](references.md) - Server + - Backend + - [Main](server/backend/main.md) - [Cli](server/cli.md) + - [Cli2](server/cli2.md) - Vendors - [Adva](vendors/adva.md) - [Base Probe](vendors/base_probe.md) - [Constants](vendors/constants.md) - - [Microsemi Twst](vendors/microsemi_twst.md) \ No newline at end of file + - Microchip + - [Tp4100](vendors/microchip/tp4100.md) + - [Twst](vendors/microchip/twst.md) + - [Ntp](vendors/ntp.md) \ No newline at end of file diff --git a/docs/api/mixins/collect.md b/docs/api/mixins/collect.md new file mode 100644 index 0000000..672fcaa --- /dev/null +++ b/docs/api/mixins/collect.md @@ -0,0 +1,7 @@ +# `opensampl.mixins.collect` + +::: opensampl.mixins.collect + options: + show_root_heading: false + show_submodules: true + show_source: true diff --git a/docs/api/mixins/random_data.md b/docs/api/mixins/random_data.md new file mode 100644 index 0000000..e9a95b8 --- /dev/null +++ b/docs/api/mixins/random_data.md @@ -0,0 +1,7 @@ +# `opensampl.mixins.random_data` + +::: opensampl.mixins.random_data + options: + show_root_heading: false + show_submodules: true + show_source: true diff --git a/docs/api/server/backend/main.md b/docs/api/server/backend/main.md new file mode 100644 index 0000000..f283af2 --- /dev/null +++ b/docs/api/server/backend/main.md @@ -0,0 +1,7 @@ +# `opensampl.server.backend.main` + +::: opensampl.server.backend.main + options: + show_root_heading: false + show_submodules: true + show_source: true diff --git a/docs/api/server/cli2.md b/docs/api/server/cli2.md new file mode 100644 index 0000000..c23781b --- /dev/null +++ b/docs/api/server/cli2.md @@ -0,0 +1,8 @@ +# CLI Reference + +This page provides documentation for our command line tools. + +::: mkdocs-click + :module: opensampl.server.cli2 + :command: cli + :prog_name: cli \ No newline at end of file diff --git a/docs/api/vendors/microchip/tp4100.md b/docs/api/vendors/microchip/tp4100.md new file mode 100644 index 0000000..6f6374e --- /dev/null +++ b/docs/api/vendors/microchip/tp4100.md @@ -0,0 +1,7 @@ +# `opensampl.vendors.microchip.tp4100` + +::: opensampl.vendors.microchip.tp4100 + options: + show_root_heading: false + show_submodules: true + show_source: true diff --git a/docs/api/vendors/microchip/twst.md b/docs/api/vendors/microchip/twst.md new file mode 100644 index 0000000..f913118 --- /dev/null +++ b/docs/api/vendors/microchip/twst.md @@ -0,0 +1,7 @@ +# `opensampl.vendors.microchip.twst` + +::: opensampl.vendors.microchip.twst + options: + show_root_heading: false + show_submodules: true + show_source: true diff --git a/docs/api/vendors/microsemi_twst.md b/docs/api/vendors/microsemi_twst.md deleted file mode 100644 index fe350c8..0000000 --- a/docs/api/vendors/microsemi_twst.md +++ /dev/null @@ -1,7 +0,0 @@ -# `opensampl.vendors.microsemi_twst` - -::: opensampl.vendors.microsemi_twst - options: - show_root_heading: false - show_submodules: true - show_source: true diff --git a/docs/api/load/probe.md b/docs/api/vendors/ntp.md similarity index 65% rename from docs/api/load/probe.md rename to docs/api/vendors/ntp.md index d650317..e280bc2 100644 --- a/docs/api/load/probe.md +++ b/docs/api/vendors/ntp.md @@ -1,6 +1,6 @@ -# `opensampl.load.probe` +# `opensampl.vendors.ntp` -::: opensampl.load.probe +::: opensampl.vendors.ntp options: show_root_heading: false show_submodules: true diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 982c177..7a99120 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -1,18 +1,41 @@ -# Installation of openSAMPL +# Installation -## Installation for Users +## User install + +OpenSAMPL requires Python 3.10 or newer. + +Install the published package with `pip`: -1. Ensure you have Python 3.9 or higher installed -2. Pip install the latest version of openSAMPL: ```bash pip install opensampl ``` -## Installation for Developers -[Poetry](https://python-poetry.org/) is used for managing dependencies. To install the package in development mode, run the following commands: +If you plan to use collection features such as remote NTP or Microchip collectors, install the optional collect dependencies: + +```bash +pip install "opensampl[collect]" +``` + +If you want the packaged Docker-backed server tooling as well: + +```bash +pip install "opensampl[server]" +``` + +## Developer Installation + +The repository uses `uv` for local development workflows. + ```bash -# Clone the repository git clone git@github.com:ORNL/OpenSAMPL.git -cd opensampl -poetry install +cd OpenSAMPL +uv venv +uv sync --all-extras --dev +source .venv/bin/activate ``` + +That installs: + +- the package into the local virtual environment +- all optional extras +- development tools such as `pytest`, `ruff`, and `mkdocs` diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index 9d5cba5..fd71e7a 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -1,16 +1,69 @@ -# Quick Start using Server +# Quickstart -By running `opensampl-server up`, you get a full stack ready to receive your time data. +This quickstart uses the packaged Docker-backed server stack. -This creates: +## Start the stack -1. A TimescaleDB instance, with schema already fully formatted -1. A BackendAPI ready to be used to ingest data - * Complete with Swagger documentation at: [http://localhost:8015](http://localhost:8015) -1. A [Grafana](https://grafana.com/) server with a dashboard already built to visualize clock data. - * Accessible at [http://localhost:3015](http://localhost:3015) +Install the server extra first: -In order to ingest data, you simply have to run `opensampl load` with appropriate arguments, and it will go straight into your new TimescaleDB -instance and become visible on Grafana. +```bash +pip install "opensampl[server]" +``` -See the [Configuration](../guides/configuration.md#opensampl-server) page on configuring your server instance. \ No newline at end of file +Then start the default stack: + +```bash +opensampl-server up +``` + +That starts: + +1. PostgreSQL / TimescaleDB on `localhost:5415` +2. the backend API on `http://localhost:8015` +3. Grafana on `http://localhost:3015` + +When `opensampl-server up` completes, it also updates your local OpenSAMPL environment so future `opensampl load ...` commands route through the backend API by default. + +## Initialize and load data + +Create the schema if needed: + +```bash +opensampl init +``` + +Load a probe file: + +```bash +opensampl load ADVA ./path/to/file.txt.gz +``` + +Or load a directory: + +```bash +opensampl load ADVA ./path/to/data-dir +``` + +The loaded data should then be visible through Grafana. + +## Inspect and stop + +Check service status: + +```bash +opensampl-server ps +``` + +Follow logs: + +```bash +opensampl-server logs +``` + +Stop the stack: + +```bash +opensampl-server down +``` + +See the [Configuration](../guides/configuration.md#opensampl-server) and [Server guide](../guides/opensampl-server.md) pages for environment customization and compose overrides. diff --git a/docs/guides/collection.md b/docs/guides/collection.md index 222c4d7..96cc903 100644 --- a/docs/guides/collection.md +++ b/docs/guides/collection.md @@ -8,10 +8,9 @@ The collect API enables automated collection of measurement data from network-co - **Microchip TWST Modems** (ATS6502 series): Collect offset and EBNO tracking values along with contextual information - **Microchip TimeProvider® 4100** (TP4100): Collect timing performance metrics from various input channels via web interface +- **NTP**: Collect local synchronization state or query remote NTP servers -## CLI Usage - -### Installation +## Installation The collect functionality is included with the main OpenSAMPL installation: @@ -19,9 +18,67 @@ The collect functionality is included with the main OpenSAMPL installation: pip install opensampl ``` -### Basic CLI Commands +For NTP or Microchip collection, install the optional collect extras so additional requirements are available: + +```bash +pip install "opensampl[collect]" +``` + + +## Basic CLI Commands + +The collect CLI for probes is accessed through the `opensampl collect` command. + +### Collecting from NTP Sources + +The NTP collector supports two modes: + +- `local`: reads host sync state from `chronyc`, `ntpq`, `timedatectl`, and `systemctl` when they are available +- `remote`: sends an NTP query to a target host and records the returned delay, offset, stratum, and metadata + +Basic command structure: + +```bash +opensampl collect ntp [OPTIONS] +``` + +#### Common options + +- `--probe-id`: stable identifier for the target probe written into `probe_metadata` +- `--output-dir`: directory to write collected files instead of loading directly +- `--load`: load the collected artifact directly into the configured database +- `--interval`: seconds between samples; `0` means collect once and exit +- `--count`: number of samples to collect when `--interval` is greater than `0` +- `--collection-id`: override the collector host reference probe id +- `--collection-ip`: override the collector host reference IP + +#### Local mode examples + +```bash +opensampl collect ntp --mode local --probe-id local-chrony --output-dir ./ntp-out +opensampl collect ntp --mode local --probe-id local-chrony --load +``` + +#### Remote mode examples + +```bash +opensampl collect ntp --mode remote --host time.cloudflare.com --probe-id public-time --output-dir ./ntp-out +opensampl collect ntp --mode remote --host 127.0.0.1 --port 10123 --probe-id mock-a --count 5 --interval 10 --load +``` + +#### NTP metadata behavior + +Each NTP collection writes metadata for two probes: + +- the collector host, marked with `reference: true` +- the target probe being measured, which stores mode, target host/port, sync status, observed sources, and any extra metadata in `ntp_metadata` + +Remote single-response collections estimate jitter from delay and root dispersion when the server does not provide enough information to compute peer jitter directly. Local chrony and `ntpq` paths keep using measured jitter when it is available. + + +## Microchip CLI Commands -The collect CLI is accessed through the `opensampl-collect` command: +The collect CLI for Microchip TP4100 and TWST is accessed through the `opensampl-collect` command: ```bash # View available collection tools @@ -516,4 +573,4 @@ For CLI usage, the logging level is controlled by the OpenSAMPL configuration sy Once data is collected, you can load it into the OpenSAMPL database using the standard load commands. The collect API is designed to generate data files that are compatible with the OpenSAMPL data loading system. -See the main OpenSAMPL documentation for details on loading collected data into the database. \ No newline at end of file +See the main OpenSAMPL documentation for details on loading collected data into the database. diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index feb910b..70c2d37 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -28,7 +28,10 @@ You can manually set `ROUTE_TO_BACKEND=false` using `opensampl config set ROUTE_ ## opensampl-server By default, the server will be created using the `docker-compose.yaml` found in opensampl/server/. If you wish to set a different compose to further control -your server, you can export the `OPENSAMPL_SERVER__COMPOSE_FILE` env var to be the full path to your own compose file. +your server, you can export the `OPENSAMPL_SERVER__COMPOSE_FILE` env var to be the full path to your own compose file. + +You can also use the `OPENSAMPL_SERVER__OVERRIDE_FILE` env var which will be applied overtop the existing default compose file. This allows a user to replace just ports +or other small changes to the existing architecture. To customize your server, specify a new env file using the `OPENSAMPL_SERVER__DOCKER_ENV_FILE` env var. It will default to the one found in opensampl/server/default.env Here is what the default environment has configured: diff --git a/docs/guides/create_probe_type.md b/docs/guides/create_probe_type.md index 2da92a2..b7282da 100644 --- a/docs/guides/create_probe_type.md +++ b/docs/guides/create_probe_type.md @@ -1,52 +1,65 @@ # Create your own Probe Type -1. you must have the repo cloned down to use this effectively -2. opensampl installed with editable (uv install -e .) -3. call `opensampl create` to create shell and populate constants - 4. include `--collect-mixin` flag to prefill those functions as well -4. add mixins, fill out functions as desired Following instructions in the comments automatically generated. +This workflow is still experimental. It is useful when you want to scaffold a new vendor / +probe family inside a local clone of the repository. -If you plan to add the new probe type to the repo via PR, you will also need to create a new migration to add the metadata table to the base db. +## Recommended setup -IF you just want to use it locally, you can either include the `--update-db` flag when calling the original `opensampl create`, or do it after the fact with -`opensampl init`. Either case will simply ensure all models in the orm file are created in the database. +1. Clone the repository locally. +2. Install OpenSAMPL in the development environment. +3. Run `opensampl create` to generate the scaffold. + 4. include `--collect-mixin` flag if you intend to implement timing collection as well to prefill those functions +4. Fill in the generated parser, metadata model, and any collector mixins you need. +5. Run `opensampl init` or `opensampl create --update-db ...` to create the new tables in the database. +```bash +git clone git@github.com:ORNL/OpenSAMPL.git +cd OpenSAMPL +uv venv +uv sync --all-extras --dev +source .venv/bin/activate +``` ---- - -Legacy documentation, may not be fully accurate. Refer to the above and any `--help` throughout for most up to date information. -Clock types can be added to generate new ORMs for different clock types via skeleton files, which can then be further -configured to report the type of clock data that is being added to the database. - -It is recommended that you only use `opensampl create` with the package cloned down. See [Installation for developers](../getting-started/installation.md#installation-for-developers) for more details on how to do so. +If you plan to contribute the new probe type back to the repository, you will also need to +add any required schema or migration updates alongside the generated code. ## Usage -Command: `opensampl create [OPTIONS]`
-Arguments: +Command: `opensampl create [OPTIONS]` + +Arguments: * `CONFIG PATH`: The path to the config file defining the new probe type Options: -* `--update-db` (`-u`): Update the database with the new probe type +* `--update-db` (`-u`): Update the database with the new probe type ## Config File Formatting -`name`: The name of the probe type. Should not have spaces or special characters outside of `-` or `_`
+`name`: The name of the probe type. It should not contain spaces or special characters +outside of `-` or `_`. + +`parser_class`: Optional. The class name for the probe implementation. By default it is +`f'{name.capitalize()}Probe'`. + +`parser_module`: Optional. The Python module name for your probe type. By default it is +`name.lower()`. + +`metadata_orm`: Optional. The SQLAlchemy ORM class name for the metadata table. By default +it is `f'{name.capitalize()}Metadata'`. -`parser_class`: Optional: The name of the object that manages your probe type. By default, will be `f'{name.capitalize()}Probe'`
-`parser_module`: Optional: The module name for your probe type (ie, stem of the python file). By default, will be `name.lower()`
-`metadata_orm`: Optional: The name of the sqlalchemy ORM representation of the database table storing your probe type's metadata. By default, will be `f'{name.capitalize()}Metadata'`
-`metadata_table`: Optional: The table name that will store your probe type's metadata. By default, will be `f'{name.lower()}_metadata'`
+`metadata_table`: Optional. The database table name for the metadata table. By default it is +`f'{name.lower()}_metadata'`. -`metadata_fields`: A dictionary of metadata fields that will be provided for your new probe type. +`metadata_fields`: A dictionary of metadata fields that will be provided for your new probe type. -* The keys of this dict will be the names of the fields, and they will become columns in the dictionary table. -* The values of the dict are optional, but when provided they are the SQLALCHEMY type of that field (defaults to `Text`) +* The keys become column names in the generated metadata table. +* The values are optional SQLAlchemy type names. If omitted, the field defaults to `Text`. -For a full example, this is the definition of the config that would create the (already existing) ADVA probe type. +For a concrete example, this is the configuration that would scaffold the existing ADVA +probe type: ```yaml name: ADVA parser_class: AdvaProbe @@ -69,4 +82,4 @@ metadata_fields: adva_status: adva_mtie_mask: adva_mask_margin: Integer -``` \ No newline at end of file +``` diff --git a/docs/guides/index.md b/docs/guides/index.md index d46d42d..d2a237f 100644 --- a/docs/guides/index.md +++ b/docs/guides/index.md @@ -1,6 +1,8 @@ # Guides -* [Configuration](configuration.md) +* [Configuration](configuration.md) * [Using the `opensampl` CLI](opensampl-cli.md) -* [Using the `opensampl-server` CLI](opensampl-server.md) -* [Using the `opensampl-collect` CLI](opensampl-cli.md) \ No newline at end of file +* [Using the `opensampl-server` CLI](opensampl-server.md) +* [Collection Guide](collection.md) +* [Random Data Guide](random-data-generation.md) +* [NTP Extension Guide](ntp-extension.md) diff --git a/docs/guides/ntp-extension.md b/docs/guides/ntp-extension.md new file mode 100644 index 0000000..a2c86b8 --- /dev/null +++ b/docs/guides/ntp-extension.md @@ -0,0 +1,105 @@ +# NTP Extension Guide + +This guide explains how NTP support was added to OpenSAMPL, what behaviors are specific to the NTP probe family, and what assumptions the dashboards and loader make when visualizing NTP-backed timing data. + +## Overview + +The NTP work adds a first-class `NTP` vendor family to OpenSAMPL rather than treating NTP snapshots as an external pre-processing concern. + +That includes: + +- a vendor/probe implementation in `opensampl.vendors.ntp` +- local and remote collection paths +- `ntp_metadata` rows for probe-specific metadata +- NTP-specific metric loading into `probe_data` +- dashboard and query changes so NTP-backed references are handled safely + +## Probe model + +NTP uses two related probe identities: + +- the target probe, which represents the NTP server or local host being measured +- the collection probe, which represents the host performing the observation + +Time-series rows are written for the target probe and use the collection probe as the reference dimension. This keeps NTP data aligned with existing OpenSAMPL reference semantics without pretending the reference is GNSS-grounded. + +## Collection modes + +The NTP probe supports two collection modes: + +- `local` + Uses locally available tools such as `chronyc`, `ntpq`, `timedatectl`, and `systemctl` to infer synchronization state and measured metrics where available. +- `remote` + Sends a single NTP query to a remote host using `ntplib` and extracts values such as offset, delay, stratum, and root dispersion. + +The main CLI path is: + +```bash +opensampl collect ntp --mode remote --host time.cloudflare.com --probe-id public-time --output-dir ./ntp-out +opensampl collect ntp --mode local --probe-id local-chrony --load +``` + +## Metadata and loading + +Each NTP artifact contains: + +- file header metadata describing the target and collection probe relationship +- time-series rows for each collected metric + +During load: + +1. the collection probe is inserted into `probe_metadata` with `reference: true` +2. the target probe is inserted into `probe_metadata` +3. NTP-specific metadata is written into `ntp_metadata` +4. each collected metric is written into `probe_data` using the collection probe as the reference + +This design keeps NTP aligned with the existing OpenSAMPL loading model and allows dashboards to filter across vendors using the same reference tables. + +## Jitter semantics + +Remote NTP responses do not always provide a true measured peer jitter value from a single sample. When OpenSAMPL has only a single remote response, it stores a documented estimate/bound derived from delay and root dispersion rather than leaving jitter empty. + +Local `chronyc` and `ntpq` paths continue to use measured jitter when those tools expose it. + +The dashboards and docs should therefore distinguish between: + +- measured jitter from local/system tooling +- estimated jitter from single-response remote NTP queries + +## Geolocation behavior + +NTP geolocation happens at metadata ingest time, not in Grafana. + +The loader uses `ENABLE_GEOLOCATE` and the geolocation helper to decide whether to create `locations` rows: + +- if an explicit override is provided, that override wins +- if geolocation is enabled and the host resolves to a public IP, OpenSAMPL can look up coordinates through `ip-api.com` +- if the host resolves to a private or loopback IP, default lab coordinates can be used +- if there is not enough information to name and place a location, location creation is skipped + +This keeps external lookup behavior isolated to ingest time and makes the resulting dashboard state reproducible from the database alone. + +## Dashboard semantics + +For NTP-backed demo paths, the word `Reference` is intentional. + +It means: + +- the OpenSAMPL reference dimension used for joins and variable resolution + +It does not mean: + +- a claim that the underlying timing source is GNSS-truth-backed + +The backend model still preserves GNSS extensibility for future probe families. The NTP work only makes the current semantics safer and more explicit. + +## Testing and CI + +The NTP work added: + +- collector tests for local and remote parsing behavior +- metadata/load tests for `ntp_metadata` and collection-probe creation +- geolocator unit tests that mock outbound lookup behavior +- integration-style seeded database tests using MockDB + +The CI workflow also installs PostgreSQL/PostGIS tooling so environments that rely on `pytest-postgresql` can still be provisioned when needed, while the default suite remains stable on developer machines that cannot easily run a local PostGIS-backed PostgreSQL instance. diff --git a/docs/guides/opensampl-cli.md b/docs/guides/opensampl-cli.md index f38eb4f..cf27934 100644 --- a/docs/guides/opensampl-cli.md +++ b/docs/guides/opensampl-cli.md @@ -1,19 +1,21 @@ # CLI -The CLI tool provides several commands. You can use `opensampl --help` (or, any deeper `opensampl [command] --help`) to get details +Use `opensampl --help` to see the top-level commands, or `opensampl --help` +for subcommand-specific options. ## Load Data ### Probe Data -Command: `opensampl load [OPTIONS]`
-Arguments: +Command: `opensampl load [OPTIONS]` +Arguments: + +* `PROBE TYPE`: The loader implementation to use for the input files +* `INPUT PATH`: A single file or a directory of files -* `PROBE TYPE`: Specify the probe type, which defines how to process the data files - * See API reference for currently supported Probe Types. - * You can also try the experimental `create` method, described [below](#create), to define your own probe type. -* `INPUT PATH`: The path to the input. Can be a single file, or a directory of files. +Supported probe types currently exposed by the CLI include `ADVA`, `MicrochipTWST`, +`MicrochipTP4100`, `NTP`, and `random`. -Options: +Options: * `--metadata` (`-m`): Only load probe metadata * `--time-data` (`-t`): Only load time series data @@ -23,41 +25,57 @@ Options: * `--chunk-size` (`-c`): Number of time data entries per batch (default: 10000) #### ADVA -The tool currently supports ADVA probe data files with the following naming convention: +The CLI supports ADVA probe data files with the following naming convention: > `CLOCK_PROBE--YYYY-MM-DD-HH-MM-SS.txt.gz` -Example: +Example: > `10.0.0.121CLOCK_PROBE-1-1-2024-01-02-18-24-56.txt.gz` -With the file format of having metadata at the beginning (on lines starting with `#`), followed by -tab separated `time value` measurements. +The file format contains metadata at the beginning on lines starting with `#`, followed by +tab-separated `time value` measurements. -As ADVA probes have all their metadata and their time data in each file, there is no need to use the `-m` or `-t` options, though if you want to skip loading one or the other it becomes useful! +As ADVA probes have metadata and time-series data in each file, there is usually no need +to split metadata and time-data loading. #### Microchip -Microchip TWST and TP4100 modems are also supported, the file names are supported as created via `opensampl-collect` +Microchip TWST and TP4100 files are supported. Collection for those devices is still done +through the dedicated `opensampl-collect` entry point, while loading is handled through +`opensampl load MicrochipTWST ...` or `opensampl load MicrochipTP4100 ...`. + +#### NTP +NTP data can be collected with the main CLI and then loaded with the `NTP` probe type. + +```bash +opensampl collect ntp --mode remote --server pool.ntp.org --output-path ./ntp-out +opensampl load NTP ./ntp-out +``` + +For local collection, point the collector at an `ntpq` output file or let it inspect the +local system directly, depending on your deployment. See the [collection guide](collection.md) +for the current collection modes and configuration options. ### Direct Table Entries -Load data directly into a database table from a file, whose format can be yaml or json. +Load data directly into a database table from a file. The file format can be YAML or JSON. -The file should contain either one dictionary (for one entry) or a list of dictionaries (for many entries) +The file should contain either one dictionary or a list of dictionaries. -Command: `opensampl load table [OPTIONS]`
-Arguments: +Command: `opensampl load table
[OPTIONS]` +Arguments: -* `TABLE NAME`: Which table to write to. +* `TABLE NAME`: Which table to write to * `INPUT PATH`: The path to the input file, whose format can be yaml or json. See the [page on expected formatting for writing to table](expected_table_format.md) to ensure the provided file matches the table's expected format. Options: * `--if-exists` (`-i`): How to handle conflicts: - - `update`: Insert non-primary-key fields that are `NULL` in existing entry (default) - - `error`: Raise an error if entry exists - - `replace`: Replace all non-primary-key fields with new values - - `ignore`: Skip if entry exists + - `update`: Insert non-primary-key fields that are `NULL` in an existing entry (default) + - `error`: Raise an error if the entry already exists + - `replace`: Replace all non-primary-key fields with new values + - `ignore`: Skip the entry if it already exists ## Configuration -See the [configuration](configuration.md) page for details on how the config command works +See the [configuration](configuration.md) page for how `opensampl config` reads and writes +environment-backed settings. ### View @@ -78,15 +96,15 @@ opensampl config show -e -v BACKEND_URL # Show specific variable with descripti ### Set Command: `opensampl config set `
-Arguments: +Arguments: * `VAR NAME`: the env var you want to change * `VAR VALUE`: the new value to set it as ## Create -** Experimental **
-Create a new probe type with scaffolding, based on a config file. -See the [Create page](create_probe_type.md) for how to use +**Experimental** +Create a new probe type scaffold from a configuration file. See the +[Create page](create_probe_type.md) for the current workflow and limitations. Command: `opensampl create [OPTIONS]`
Arguments: @@ -95,6 +113,5 @@ Arguments: Options: -* `--update-db` (`-u`): Update the database with the new probe type - +* `--update-db` (`-u`): Update the database with the new probe type diff --git a/docs/guides/opensampl-server.md b/docs/guides/opensampl-server.md index e260000..5755e0c 100644 --- a/docs/guides/opensampl-server.md +++ b/docs/guides/opensampl-server.md @@ -1,187 +1,127 @@ # openSAMPL Server CLI Usage Guide -This guide explains how to use the openSAMPL Server command-line interface (CLI) tool to manage your openSAMPL server deployment. +The `opensampl-server` CLI is a thin wrapper around the packaged Docker Compose stack in `opensampl.server`. -## Overview - -The openSAMPL Server CLI provides commands for managing a Docker Compose deployment of the openSAMPL server infrastructure. It handles: - -- Starting and stopping services -- Viewing logs -- Checking service status -- Running custom commands +It is useful when you want a local database, backend API, migrations container, and Grafana instance without managing the compose arguments yourself. ## Prerequisites -- Docker and Docker Compose installed -- The openSAMPL Python package installed +- Docker with either `docker compose` or `docker-compose` +- OpenSAMPL installed with the server extra -Install openSAMPL as normal: -``` -pip install opensampl +```bash +pip install "opensampl[server]" ``` -## Basic Commands +## What `opensampl-server up` does -### Starting the Server - -To start the openSAMPL server: +Running: ```bash opensampl-server up ``` -This command: -- Starts all services defined in the Docker Compose file -- Runs containers in detached mode (`-d`) -- Sets local environment variables to route `opensampl load` commands via the backend +starts the packaged compose stack in detached mode and updates your local OpenSAMPL environment so future `opensampl load ...` commands route through the backend API. -**Starting a specific service:** +Specifically, it writes: -```bash -opensampl-server up --service grafana -``` -this will start up just the specified container, choices are: -- db -- backend -- grafana -- migrations +- `ROUTE_TO_BACKEND=true` +- `BACKEND_URL=http://localhost:8015` +- `DATABASE_URL=postgresql://...@localhost:5415/...` -**Using a custom .env file:** +The default stack exposes: -```bash -opensampl-server up --env-file /path/to/custom.env -``` -uses default.env with values: -```dotenv -COMPOSE_PROJECT_NAME=opensampl +- PostgreSQL / TimescaleDB on `localhost:5415` +- the backend API on `localhost:8015` +- Grafana on `localhost:3015` -POSTGRES_DB=castdb -POSTGRES_USER=castuser -POSTGRES_PASSWORD=castpassword +## Commands -DB_URI="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}" -GF_SECURITY_ADMIN_PASSWORD=secret +### Start all services -BACKEND_LOG_LEVEL=DEBUG +```bash +opensampl-server up ``` -### Stopping the Server -To stop all services: +### Start only selected services + +Additional arguments after `up` are passed through to Docker Compose. For example: ```bash -opensampl-server down +opensampl-server up grafana +opensampl-server up db backend ``` -**Stopping a specific service:** +### Stop services ```bash -opensampl-server down --service db +opensampl-server down ``` -### Viewing Logs - -To view logs from all containers: +### Show logs ```bash opensampl-server logs ``` -This command follows the logs (`-f`), showing new log entries as they arrive. - -### Checking Service Status - -To see the status of all services: +### Show service status ```bash opensampl-server ps ``` -This displays: -- Container names -- Status (running, stopped, etc.) -- Ports -- Other container information - -### Running Custom Commands - -The `run` command allows you to execute commands in a service container: +### Run a one-off compose command ```bash opensampl-server run backend python -m opensampl.cli init ``` -This example: -- Creates a temporary container for the `backend` service -- Runs the specified command -- Removes the container after completion (`--rm`) +This maps directly to `docker compose run --rm ...`. -## Environment Configuration +## Using a custom env file -By default, the CLI uses the packaged `default.env` file for configuration. You can specify a custom environment file with the `--env-file` option for any command: +`--env-file` is a top-level CLI option, so it must appear before the subcommand: ```bash -opensampl-server up --env-file ./my-custom-env.env +opensampl-server --env-file ./dev.env up +opensampl-server --env-file ./dev.env ps ``` -## Technical Details +By default, the server wrapper uses the packaged `default.env` values through `ServerConfig`. -- The CLI automatically detects whether to use `docker-compose` or `docker compose` based on your system configuration -- When starting the server, it configures your local environment to use the backend by setting: - - `BACKEND_URL=http://localhost:8015` - - `ROUTE_TO_BACKEND=true` +The shipped defaults include: -## Power users - -For those who are more familiar with docker, there is a `opensampl-server2` which corresponds to the following, more directly -exposing the docker to users. -`OPENSAMPL_SERVER__COMPOSE_FILE` is set in your .env file or environment. - -```bash -opensampl-server2 --env-file ENV_FILE args -docker compose --env-file ${ENV_FILE} -f ${OPENSAMPL_SERVER__COMPOSE_FILE} $@ +```dotenv +COMPOSE_PROJECT_NAME=opensampl +POSTGRES_DB=castdb +POSTGRES_USER=castuser +POSTGRES_PASSWORD=castpassword +GF_SECURITY_ADMIN_PASSWORD=secret +BACKEND_LOG_LEVEL=DEBUG +USE_API_KEY=false +API_KEYS=changeme123 ``` -## Troubleshooting +## Advanced configuration + +The server wrapper can be redirected to other compose files or docker env files with the server-specific environment variables described in the [configuration guide](configuration.md#opensampl-server): -If you encounter issues: +- `OPENSAMPL_SERVER__COMPOSE_FILE` +- `OPENSAMPL_SERVER__OVERRIDE_FILE` +- `OPENSAMPL_SERVER__DOCKER_ENV_FILE` -1. Check that Docker and Docker Compose are installed and running -2. Verify your environment file contains the necessary configuration -3. Check the logs for specific error messages: `opensampl-server logs` +## Troubleshooting -## Examples +1. Confirm Docker is installed and running. +2. Run `opensampl-server ps` to see whether services came up. +3. Run `opensampl-server logs` to inspect startup failures. +4. If you changed compose or env settings, confirm the files exist and are readable. -**Full development workflow:** +## Example workflow ```bash -# Start the entire stack opensampl-server up - -# Check that all services are running opensampl-server ps - -# View logs to ensure everything started correctly -opensampl-server logs - -# load clock data into the db -opensampl load probe ADVA ./data - -# When finished, shut down the stack +opensampl load ADVA ./data opensampl-server down ``` - -**Development with custom environment:** - -```bash -# Create a custom environment file -cp $(opensampl-server get-default-env) ./dev.env - -# Edit the file with custom settings -nano ./dev.env - -# Start the server with custom environment -opensampl-server up --env-file ./dev.env -``` - - diff --git a/docs/index.md b/docs/index.md index 56e5125..219c4be 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,47 +1,40 @@ # openSAMPL Documentation -python tools for adding clock data to a timescale db. -## Overview +Python tools for loading, storing, and visualizing clock data in PostgreSQL / TimescaleDB. -`opensampl` is a package released by the Oak Ridge National Laboratory (ORNL) that provides tools for adding clock -data to a postgres database. +## Overview -The package includes a CLI tool that allows users to load data from ADVA probes into a -PostgreSQL database (preferably a [TimescaleDB](https://www.timescale.com/) flavor of PostgreSQL, but it can work with -any PostgreSQL-based databases) +`opensampl` is an Oak Ridge National Laboratory (ORNL) project for synchronization analytics and monitoring data. -The tool supports loading both metadata and time series data from files, with options to -skip loading one or the other. It also provides features for archiving processed files and setting the maximum number -of worker threads for parallel processing, and helper functions for easily adding additional clocks to the database. +The package provides: -There is also an optional `server` extra which can be used to set your environment up more easily. +- a CLI for loading probe files and direct table data +- collection tooling for supported probe families +- optional Docker-backed server helpers +- Grafana dashboards and supporting backend components -## Getting Started +OpenSAMPL currently supports probe families including ADVA, Microchip TWST, Microchip TP4100, and NTP. -If you're new here, start your journey: +## Getting started -- **Installation:** Follow the steps in [Installation](getting-started/installation.md) to arm yourself with the essentials. -- **Quickstart:** For a rapid initiation, dive into our [Quickstart](getting-started/quickstart.md) guide. +- [Installation](getting-started/installation.md) +- [Quickstart](getting-started/quickstart.md) ## Guides -Master the art of customization and configuration: +- [Configuration](guides/configuration.md) +- [Using the `opensampl` CLI](guides/opensampl-cli.md) +- [Using the `opensampl-server` CLI](guides/opensampl-server.md) +- [Collection Guide](guides/collection.md) +- [Random Data Guide](guides/random-data-generation.md) +- [NTP Extension Guide](guides/ntp-extension.md) -[//]: # (* [Configuration](guides/configuration.md) – Learn how to set up and tweak `opensampl` to your liking.) -* [Customization](guides/configuration) – Discover advanced tweaks that put you in complete control. -* [Using the `opensampl` CLI](guides/opensampl-cli.md) – A comprehensive guide to using the `opensampl` CLI tool. -* [Using the `opensampl-server` CLI](guides/opensampl-server.md) – A comprehensive guide to using the `opensampl-server` CLI tool. +## API references -## API Reference +- [openSAMPL API Reference](api/index.md) -Dare to explore our API: +## Repository -* [openSAMPL API Reference](python-api-reference.md) – A comprehensive look into every function, class, and module that defines `opensampl` -* [Server API Reference](server-api-reference.md) – A detailed breakdown of the Python API that powers `opensampl`. ---- - -Each word here is designed to illuminate your path through the codebase. If you find yourself lost, refer back to this -document for guidance. And remember, the journey of a thousand commits begins with a single `git clone`. 🚀 -``` +```bash git clone git@github.com:ORNL/OpenSAMPL.git ``` diff --git a/docs/python-api-reference.md b/docs/python-api-reference.md deleted file mode 100644 index 8cd1efa..0000000 --- a/docs/python-api-reference.md +++ /dev/null @@ -1,18 +0,0 @@ -# openSAMPL API Reference - -This section contains the automatically generated API reference for the openSAMPL library. - -## Module Index - -* [cli](api/cli.md) -* [constants](api/constants.md) -* [load_data](api/load_data.md) -* **db/** - * [access_orm](api/db/access_orm.md) - * [orm](api/db/orm.md) -* **server/** - * [cli](api/server/cli.md) -* **vendors/** - * [adva](api/vendors/adva.md) - * [base_probe](api/vendors/base_probe.md) - * [constants](api/vendors/constants.md) diff --git a/docs/server-api-reference.md b/docs/server-api-reference.md deleted file mode 100644 index 887de4f..0000000 --- a/docs/server-api-reference.md +++ /dev/null @@ -1,3 +0,0 @@ -# The `opensampl` API -The `opensampl[server]` API is a RESTful API that is built using FastAPI. The API is designed to be a backend API for -clocks to push data to. \ No newline at end of file diff --git a/mkdocs.yaml b/mkdocs.yaml index 848aee3..c439a1f 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -14,38 +14,57 @@ nav: - Create: guides/create_probe_type.md - Server: guides/opensampl-server.md - Collect: guides/collection.md + - Automatic Ingest: guides/automatic_ingest.md + - NTP Extension: guides/ntp-extension.md - Random Data: guides/random-data-generation.md - API: - index: api/index.md - - ats6502: - - context: api/ats6502/context.md - - modem: api/ats6502/modem.md - - readings: api/ats6502/readings.md - cli: api/cli.md + - collect: + - cli: api/collect/cli.md + - microchip: + - tp4100: + - collect_4100: api/collect/microchip/tp4100/collect_4100.md + - twst: + - context: api/collect/microchip/twst/context.md + - generate_twst_files: api/collect/microchip/twst/generate_twst_files.md + - readings: api/collect/microchip/twst/readings.md + - modem: api/collect/modem.md - config: - base: api/config/base.md + - server: api/config/server.md + - tp4100: api/config/tp4100.md + - create: + - create_vendor: api/create/create_vendor.md + - insert_markers: api/create/insert_markers.md - db: - access_orm: api/db/access_orm.md - orm: api/db/orm.md - helpers: - - create_vendor: api/helpers/create_vendor.md - - insert_markers: api/helpers/insert_markers.md - - source_writer: api/helpers/source_writer.md + - geolocator: api/helpers/geolocator.md - load: - data: api/load/data.md - - probe: api/load/probe.md - routing: api/load/routing.md - table_factory: api/load/table_factory.md - load_data: api/load_data.md - metrics: api/metrics.md + - mixins: + - collect: api/mixins/collect.md + - random_data: api/mixins/random_data.md - references: api/references.md - server: + - backend: + - main: api/server/backend/main.md - cli: api/server/cli.md + - cli2: api/server/cli2.md - vendors: - adva: api/vendors/adva.md - base_probe: api/vendors/base_probe.md - constants: api/vendors/constants.md - - microsemi_twst: api/vendors/microsemi_twst.md + - microchip: + - tp4100: api/vendors/microchip/tp4100.md + - twst: api/vendors/microchip/twst.md + - ntp: api/vendors/ntp.md plugins: - search - mkdocstrings: diff --git a/opensampl/cli.py b/opensampl/cli.py index 91f2369..3137c44 100644 --- a/opensampl/cli.py +++ b/opensampl/cli.py @@ -9,7 +9,7 @@ import os import sys from pathlib import Path -from typing import Literal, Optional, Union +from typing import Literal import click import yaml @@ -35,7 +35,7 @@ """ -def load_config(env_file: Optional[str] = None) -> CLIConfig: +def load_config(env_file: str | None = None) -> CLIConfig: """ Load the configuration settings @@ -64,7 +64,7 @@ def load_config(env_file: Optional[str] = None) -> CLIConfig: class CaseInsensitiveGroup(click.Group): """Defines Click group options as case-insensitive. By default, click groups are case-sensitive.""" - def get_command(self, ctx: click.Context, cmd_name: str) -> Optional[click.Command]: # noqa: ARG002 + def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None: # noqa: ARG002 """Normalize command name to lower case""" cmd_name = cmd_name.lower() # Match against lowercased command names @@ -200,7 +200,7 @@ def collect(): collect.add_command(_vend.get_collect_cli_command(), name=vendor.name) -def path_or_string(value: str) -> Union[dict, list]: +def path_or_string(value: str) -> dict | list: """Get content from a file or use the string directly""" # Get content - either from file or use the string directly content = value @@ -236,9 +236,7 @@ def path_or_string(value: str) -> Union[dict, list]: ) @click.argument("table_name", type=click.Choice(get_table_names())) @click.argument("filepath", type=path_or_string) -def table_load( - filepath: Union[dict, list], table_name: str, if_exists: Literal["update", "error", "replace", "ignore"] -): +def table_load(filepath: dict | list, table_name: str, if_exists: Literal["update", "error", "replace", "ignore"]): r""" Perform a Table load into the database. diff --git a/opensampl/collect/cli.py b/opensampl/collect/cli.py index e65df6e..6a07e69 100644 --- a/opensampl/collect/cli.py +++ b/opensampl/collect/cli.py @@ -1,7 +1,7 @@ """Consolidated CLI entry point for opensampl.collect tools.""" import sys -from typing import Literal, Optional +from typing import Literal import click from loguru import logger @@ -84,8 +84,8 @@ def tp4100( port: int, output_dir: str, duration: int, - channels: Optional[list[str]], - metrics: Optional[list[str]], + channels: list[str] | None, + metrics: list[str] | None, method: Literal["chart_data", "download_file"], save_full_status: bool, verbose: bool, diff --git a/opensampl/collect/microchip/tp4100/__init__.py b/opensampl/collect/microchip/tp4100/__init__.py index d2434d0..9246439 100644 --- a/opensampl/collect/microchip/tp4100/__init__.py +++ b/opensampl/collect/microchip/tp4100/__init__.py @@ -7,7 +7,7 @@ """ from dataclasses import dataclass -from typing import ClassVar, Optional +from typing import ClassVar @dataclass @@ -76,7 +76,7 @@ class MonitoringConfig: channel_str: str ids: list[int] metrics: list[MetricInfo] - default_download: Optional[dict] = None + default_download: dict | None = None @property def download_path(self): @@ -90,7 +90,7 @@ def download_path(self): return f"perfmon_{self.channel_str}_stat" def download_payload( - self, download: Optional[dict] = None, which_id: int = 1, down_metric: MetricInfo = MONITOR_METRIC.TE + self, download: dict | None = None, which_id: int = 1, down_metric: MetricInfo = MONITOR_METRIC.TE ): """ Generate payload for data download requests. diff --git a/opensampl/collect/microchip/tp4100/collect_4100.py b/opensampl/collect/microchip/tp4100/collect_4100.py index f3de6ba..20de8f4 100644 --- a/opensampl/collect/microchip/tp4100/collect_4100.py +++ b/opensampl/collect/microchip/tp4100/collect_4100.py @@ -11,7 +11,7 @@ from datetime import datetime, timezone from pathlib import Path from pprint import pformat -from typing import Any, Literal, Optional +from typing import Any, Literal import pandas as pd import requests @@ -44,8 +44,8 @@ def __init__( port: int = 443, output_dir: str = "./output", duration: int = 600, - channels: Optional[list[str]] = None, - metrics: Optional[list[str]] = None, + channels: list[str] | None = None, + metrics: list[str] | None = None, method: Literal["chart_data", "download_file"] = "chart_data", save_full_status: bool = False, ): @@ -171,7 +171,7 @@ def collect_readings(self): elif self.method == "download_file": self.download_files(request_tpl) - def get_filename(self, detail: Optional[str] = None, extension: str = ".txt"): + def get_filename(self, detail: str | None = None, extension: str = ".txt"): """ Generate timestamped filename for probe connection @@ -194,7 +194,7 @@ def get_filename(self, detail: Optional[str] = None, extension: str = ".txt"): return filename def collect_chart_data( - self, request_key: tuple[MonitoringConfig, int, MetricInfo], download_dict: Optional[dict[str, Any]] = None + self, request_key: tuple[MonitoringConfig, int, MetricInfo], download_dict: dict[str, Any] | None = None ): """ Collect chart data for a specific metric and channel. @@ -271,7 +271,7 @@ def format_utc_second(tai_sec: str, utc_offset: str) -> datetime: df.to_csv(new_file, mode="a", index=False) def download_files( - self, request_key: tuple[MonitoringConfig, int, MetricInfo], download_dict: Optional[dict[str, Any]] = None + self, request_key: tuple[MonitoringConfig, int, MetricInfo], download_dict: dict[str, Any] | None = None ): """ Download data files directly from the device. @@ -341,8 +341,8 @@ def main( port: int = 443, output_dir: str = "./output", duration: int = 600, - channels: Optional[list[str]] = None, - metrics: Optional[list[str]] = None, + channels: list[str] | None = None, + metrics: list[str] | None = None, method: Literal["chart_data", "download_file"] = "chart_data", save_full_status: bool = False, ): diff --git a/opensampl/collect/microchip/twst/context.py b/opensampl/collect/microchip/twst/context.py index 688a39e..3d4f2fc 100644 --- a/opensampl/collect/microchip/twst/context.py +++ b/opensampl/collect/microchip/twst/context.py @@ -9,7 +9,7 @@ import textwrap from datetime import datetime, timezone from types import SimpleNamespace -from typing import Any, Optional +from typing import Any import yaml from loguru import logger @@ -54,7 +54,7 @@ def finished_ok(line: str) -> bool: return re.match(r"^\[OK\]", line) is not None @staticmethod - def finished_error(line: str) -> tuple[bool, Optional[str]]: + def finished_error(line: str) -> tuple[bool, str | None]: """ Check if a command completed with an error. diff --git a/opensampl/collect/microchip/twst/generate_twst_files.py b/opensampl/collect/microchip/twst/generate_twst_files.py index 5f145dc..da15612 100644 --- a/opensampl/collect/microchip/twst/generate_twst_files.py +++ b/opensampl/collect/microchip/twst/generate_twst_files.py @@ -24,7 +24,6 @@ import csv import time from pathlib import Path -from typing import Optional from loguru import logger @@ -57,7 +56,7 @@ def collect_files( status_port: int = 1900, output_dir: str = "./output", dump_interval: int = 300, - total_duration: Optional[int] = None, + total_duration: int | None = None, ): """ Continuously collect blocks of modem measurements and save to timestamped CSV files. diff --git a/opensampl/collect/microchip/twst/readings.py b/opensampl/collect/microchip/twst/readings.py index 622cce3..02fb712 100644 --- a/opensampl/collect/microchip/twst/readings.py +++ b/opensampl/collect/microchip/twst/readings.py @@ -6,7 +6,6 @@ """ import asyncio -from typing import Optional from loguru import logger @@ -23,7 +22,7 @@ class ModemStatusReader(ModemReader): status readings over a specified duration. """ - def __init__(self, host: str, duration: int = 60, keys: Optional[list[str]] = None, port: int = 1900): + def __init__(self, host: str, duration: int = 60, keys: list[str] | None = None, port: int = 1900): """ Initialize ModemStatusReader. diff --git a/opensampl/collect/modem.py b/opensampl/collect/modem.py index 95b62cc..d602aa0 100644 --- a/opensampl/collect/modem.py +++ b/opensampl/collect/modem.py @@ -5,9 +5,9 @@ for interfacing with modems via telnet. """ +from collections.abc import Callable from contextlib import asynccontextmanager from functools import wraps -from typing import Callable, Optional from loguru import logger @@ -36,7 +36,7 @@ def require_conn(method: Callable): """ @wraps(method) - async def wrapper(self: "ModemReader", *args: list, **kwargs: dict) -> Optional[Callable]: + async def wrapper(self: "ModemReader", *args: list, **kwargs: dict) -> Callable | None: if not getattr(self, "open", False): raise RuntimeError( "Telnet connection not active: reader/writer cannot be used outside of 'async with connect()'" @@ -72,8 +72,8 @@ def __init__( self.host = host self.port = port self.encoding = encoding - self.reader: Optional[telnetlib3.TelnetReader] = None - self.writer: Optional[telnetlib3.TelnetWriter] = None + self.reader: telnetlib3.TelnetReader | None = None + self.writer: telnetlib3.TelnetWriter | None = None self.open: bool = False @asynccontextmanager diff --git a/opensampl/config/base.py b/opensampl/config/base.py index aa2af28..145b1f5 100644 --- a/opensampl/config/base.py +++ b/opensampl/config/base.py @@ -6,7 +6,7 @@ """ from pathlib import Path -from typing import Any, Optional +from typing import Any from dotenv import set_key from loguru import logger @@ -28,21 +28,27 @@ class BaseConfig(BaseSettings): ROUTE_TO_BACKEND: bool = Field( False, description="URL of the backend service when routing is enabled", alias="ROUTE_TO_BACKEND" ) - BACKEND_URL: Optional[str] = Field( + BACKEND_URL: str | None = Field( None, description="URL of the backend service when routing is enabled", alias="BACKEND_URL" ) - DATABASE_URL: Optional[str] = Field(None, description="URL for direct database connections", alias="DATABASE_URL") + DATABASE_URL: str | None = Field(None, description="URL for direct database connections", alias="DATABASE_URL") ARCHIVE_PATH: Path = Field( Path("archive"), description="Default path that files are moved to after they have been processed", alias="ARCHIVE_PATH", ) LOG_LEVEL: str = Field("INFO", description="Log level for opensampl cli", alias="LOG_LEVEL") - API_KEY: Optional[str] = Field(None, description="Access key for interacting with the backend", alias="API_KEY") + API_KEY: str | None = Field(None, description="Access key for interacting with the backend", alias="API_KEY") INSECURE_REQUESTS: bool = Field( False, description="Allow insecure requests to be made to the backend", alias="INSECURE_REQUESTS" ) + ENABLE_GEOLOCATE: bool = Field( + False, + description="Enable geolocate features which extract a location from ip addresses", + alias="ENABLE_GEOLOCATE", + ) + @field_serializer("ARCHIVE_PATH") def convert_to_str(self, v: Path) -> str: """Convert archive path to a string for serialization""" @@ -111,7 +117,7 @@ def set_by_name(self, name: str, value: Any): set_key(self.env_file, name, str(value)) - def save_config(self, values: Optional[list[str]] = None): + def save_config(self, values: list[str] | None = None): """ Save the current env configuration. diff --git a/opensampl/config/server.py b/opensampl/config/server.py index 6478145..16d061b 100644 --- a/opensampl/config/server.py +++ b/opensampl/config/server.py @@ -5,11 +5,12 @@ configuration validation, and settings management. """ +from __future__ import annotations + import shlex from importlib.resources import as_file, files from pathlib import Path -from types import ModuleType -from typing import Any, Union +from typing import TYPE_CHECKING, Any from dotenv import dotenv_values, set_key from loguru import logger @@ -20,8 +21,11 @@ from opensampl.config.base import BaseConfig from opensampl.server import check_command +if TYPE_CHECKING: + from types import ModuleType + -def get_resolved_resource_path(pkg: Union[str, ModuleType], relative_path: str) -> str: +def get_resolved_resource_path(pkg: str | ModuleType, relative_path: str) -> str: """Retrieve the resolved path to a resource in a package.""" resource = files(pkg).joinpath(relative_path) with as_file(resource) as real_path: @@ -35,6 +39,8 @@ class ServerConfig(BaseConfig): COMPOSE_FILE: str = Field(default="", description="Fully resolved path to the Docker Compose file.") + OVERRIDE_FILE: str | None = Field(default=None, description="Override for the compose file") + DOCKER_ENV_FILE: str = Field(default="", description="Fully resolved path to the Docker .env file.") docker_env_values: dict[str, Any] = Field(default_factory=dict, init=False) @@ -54,7 +60,7 @@ def _ignore_in_set(self) -> list[str]: return ignored @model_validator(mode="after") - def get_docker_values(self) -> "ServerConfig": + def get_docker_values(self) -> ServerConfig: """Get the values that the docker containers will use on startup""" self.docker_env_values = dotenv_values(self.DOCKER_ENV_FILE) return self @@ -67,6 +73,14 @@ def resolve_compose_file(cls, v: Any) -> str: return get_resolved_resource_path(opensampl.server, "docker-compose.yaml") return str(Path(v).expanduser().resolve()) + @field_validator("OVERRIDE_FILE", mode="before") + @classmethod + def resolve_override_file(cls, v: Any) -> str: + """Resolve the provided compose file for docker to use, or default to the docker-compose.yaml provided""" + if v: + return str(Path(v).expanduser().resolve()) + return v + @field_validator("DOCKER_ENV_FILE", mode="before") @classmethod def resolve_docker_env_file(cls, v: Any) -> str: @@ -89,6 +103,8 @@ def build_docker_compose_base(self): compose_command = self.get_compose_command() command = shlex.split(compose_command) command.extend(["--env-file", self.DOCKER_ENV_FILE, "-f", self.COMPOSE_FILE]) + if self.OVERRIDE_FILE: + command.extend(["-f", self.OVERRIDE_FILE]) return command def set_by_name(self, name: str, value: Any): diff --git a/opensampl/create/create_vendor.py b/opensampl/create/create_vendor.py index 5663759..0a8d0d6 100644 --- a/opensampl/create/create_vendor.py +++ b/opensampl/create/create_vendor.py @@ -14,7 +14,7 @@ from pathlib import Path from pprint import pformat from string import Template -from typing import Any, Optional, Union +from typing import Any import yaml from loguru import logger @@ -36,8 +36,8 @@ class MetadataField(BaseModel): """ name: str - sqlalchemy_type: Optional[str] = Field(default="Text") - primary_key: Optional[bool] = False + sqlalchemy_type: str | None = Field(default="Text") + primary_key: bool | None = False class DEFAULT_METADATA: # noqa N801 @@ -70,7 +70,7 @@ class VendorConfig(VendorType): metadata_fields: list[MetadataField] @classmethod - def from_config_file(cls, config_path: Union[str, Path]) -> "VendorConfig": + def from_config_file(cls, config_path: str | Path) -> "VendorConfig": """ Convert file config into Config object. diff --git a/opensampl/create/templates/collect_mixin_template.txt b/opensampl/create/templates/collect_mixin_template.txt index 8983309..65b8670 100644 --- a/opensampl/create/templates/collect_mixin_template.txt +++ b/opensampl/create/templates/collect_mixin_template.txt @@ -30,7 +30,7 @@ class $parser_class(BaseProbe, CollectMixin): def __init__(self, input_file: str, **kwargs): """Initialize $parser_class from input file""" - super().__init__(input_file) + super().__init__(input_file, **kwargs) # TODO: parse self.input_file contents or file name to extract ip_address and id to self.probe_key # self.probe_key = ProbeKey(probe_id=..., ip_address=...) diff --git a/opensampl/create/templates/parser_template.txt b/opensampl/create/templates/parser_template.txt index 04be83a..648c618 100644 --- a/opensampl/create/templates/parser_template.txt +++ b/opensampl/create/templates/parser_template.txt @@ -13,7 +13,7 @@ class $parser_class(BaseProbe): def __init__(self, input_file: str, **kwargs): """Initialize $parser_class from input file""" - super().__init__(input_file) + super().__init__(input_file, **kwargs) # TODO: parse self.input_file contents or file name to extract ip_address and id to self.probe_key # self.probe_key = ProbeKey(probe_id=..., ip_address=...) diff --git a/opensampl/db/access_orm.py b/opensampl/db/access_orm.py index 715b6f1..e07f052 100644 --- a/opensampl/db/access_orm.py +++ b/opensampl/db/access_orm.py @@ -3,7 +3,7 @@ import secrets import uuid from datetime import datetime, timezone -from typing import Optional, Union +from typing import Optional from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text from sqlalchemy.orm import Session, declarative_base, relationship @@ -53,7 +53,7 @@ class Views(Base): name = Column(Text) @staticmethod - def get_view_by_name(session: Session, name: str) -> Optional[type["Views"]]: + def get_view_by_name(session: Session, name: str) -> type["Views"] | None: """ Get view by name. @@ -81,7 +81,7 @@ class Roles(Base): view_id = Column(Text, ForeignKey("views.view_id")) @staticmethod - def get_role_by_name(session: Session, name: str) -> Optional[type["Roles"]]: + def get_role_by_name(session: Session, name: str) -> type["Roles"] | None: """ Get role by name. @@ -135,7 +135,7 @@ class UserRole(Base): role_id = Column(Text, ForeignKey("roles.role_id"), primary_key=True) -def add_user_role(emails: Union[str, list[str]], role_name: str, session: Session): +def add_user_role(emails: str | list[str], role_name: str, session: Session): """ Add user role to the database. diff --git a/opensampl/db/orm.py b/opensampl/db/orm.py index ee6b374..2e104e6 100644 --- a/opensampl/db/orm.py +++ b/opensampl/db/orm.py @@ -2,7 +2,7 @@ import uuid from datetime import datetime -from typing import Any, Optional +from typing import Any from geoalchemy2 import Geometry, WKTElement from geoalchemy2.shape import to_shape @@ -43,7 +43,7 @@ def convert_value(value: Any) -> Any: return {c.name: convert_value(getattr(self, c.name)) for c in self.__table__.columns} @classmethod - def identifiable_constraint(cls) -> Optional[str]: + def identifiable_constraint(cls) -> str | None: """ Get the name of the unique constraint used for identification. @@ -57,7 +57,7 @@ def identifiable_constraint(cls) -> Optional[str]: """ return None - def resolve_references(self, session: Optional[Session] = None) -> None: # noqa: ARG002 + def resolve_references(self, session: Session | None = None) -> None: # noqa: ARG002 """ Resolve UUIDs for other entries in the database given a unique constraint. @@ -181,6 +181,7 @@ class ProbeMetadata(Base): adva_metadata = relationship("AdvaMetadata", back_populates="probe", uselist=False) microchip_twst_metadata = relationship("MicrochipTWSTMetadata", back_populates="probe", uselist=False) microchip_tp4100_metadata = relationship("MicrochipTP4100Metadata", back_populates="probe", uselist=False) + ntp_metadata = relationship("NtpMetadata", back_populates="probe", uselist=False) # --- CUSTOM PROBE METADATA RELATIONSHIP --- @@ -202,7 +203,7 @@ def __init__(self, **kwargs: Any): self._test_name = test_name @classmethod - def identifiable_constraint(cls) -> Optional[str]: + def identifiable_constraint(cls) -> str | None: """ Get the name of the unique constraint used for identification. @@ -212,7 +213,7 @@ def identifiable_constraint(cls) -> Optional[str]: """ return "uq_probe_metadata_ipaddress_probeid" - def resolve_references(self, session: Optional[Session] = None): + def resolve_references(self, session: Session | None = None): """ Resolve references to location and/or test entries when given just the name. @@ -433,8 +434,30 @@ class MicrochipTP4100Metadata(Base): probe = relationship("ProbeMetadata", back_populates="microchip_tp4100_metadata") +class NtpMetadata(Base): + """NTP Clock Probe specific metadata""" + + __tablename__ = "ntp_metadata" + + probe_uuid = Column(String, ForeignKey("probe_metadata.uuid"), primary_key=True) + mode = Column(Text) + reference = Column(Boolean, comment="Is used as a reference for other probes") + target_host = Column(Text) + target_port = Column(Integer) + sync_status = Column(Text) + leap_status = Column(Text) + reference_id = Column(Text) + observation_sources = Column(JSONB) + collection_id = Column(Text) + collection_ip = Column(Text) + timeout = Column(Float) + additional_metadata = Column(JSONB) + probe = relationship("ProbeMetadata", back_populates="ntp_metadata") + + # --- CUSTOM TABLES --- !! Do not remove line, used as reference when inserting metadata table + # --- TABLE FUNCTIONS --- diff --git a/opensampl/helpers/geolocator.py b/opensampl/helpers/geolocator.py new file mode 100644 index 0000000..adf2be4 --- /dev/null +++ b/opensampl/helpers/geolocator.py @@ -0,0 +1,124 @@ +"""Associate NTP probes with ``castdb.locations`` for the geospatial Grafana dashboard.""" + +from __future__ import annotations + +import ipaddress +import json +import os +import socket +import urllib.request +from typing import TYPE_CHECKING + +from loguru import logger + +from opensampl.load.table_factory import TableFactory + +if TYPE_CHECKING: + from sqlalchemy.orm import Session + + +_GEO_CACHE: dict[str, tuple[float, float, str]] = {} + + +def _env_bool(name: str, default: bool) -> bool: + v = os.getenv(name) + if v is None: + return default + return v.strip().lower() in ("1", "true", "yes", "on") + + +def _default_lab_coords() -> tuple[float, float]: + lat = float(os.getenv("DEFAULT_LAT", "35.9312")) + lon = float(os.getenv("DEFAULT_LON", "-84.3101")) + return lat, lon + + +def _is_private_or_loopback(ip: str) -> bool: + try: + addr = ipaddress.ip_address(ip) + except ValueError: + return True + return bool(addr.is_private or addr.is_loopback or addr.is_link_local or addr.is_reserved) + + +def _lookup_geo_ipapi(ip: str) -> tuple[float, float, str] | None: + if ip in _GEO_CACHE: + return _GEO_CACHE[ip] + url = f"http://ip-api.com/json/{ip}?fields=status,lat,lon,city,country" + try: + with urllib.request.urlopen(url, timeout=4.0) as resp: # noqa: S310 + body = json.loads(resp.read().decode("utf-8")) + except Exception as e: + logger.warning("ip-api geolocation failed for {}: {}", ip, e) + return None + + if body.get("status") != "success" or body.get("lat") is None or body.get("lon") is None: + logger.warning("ip-api returned no coordinates for {}", ip) + return None + + city = body.get("city") or "" + country = body.get("country") or "" + label = ", ".join(x for x in (city, country) if x) + out = (float(body["lat"]), float(body["lon"]), label or ip) + _GEO_CACHE[ip] = out + return out + + +def create_location(session: Session, geolocate_enabled: bool, ip_address: str, geo_override: dict) -> str | None: + """ + Set probe ``name``, ``public``, and ``location_uuid`` on NTP metadata before ``probe_metadata`` insert. + + Uses ``additional_metadata.geo_override`` when present (lat/lon/label). Otherwise resolves the remote + host, uses RFC1918/loopback defaults from env, or ip-api.com for public IPs (HTTP, no API key). + """ + lat: float | None = None + lon: float | None = None + name: str | None = None + + if geo_override.get("lat") is not None and geo_override.get("lon") is not None: + lat = float(geo_override["lat"]) + lon = float(geo_override["lon"]) + + name = geo_override.get("name") + + if geolocate_enabled and lat is None and lon is None: + ip_for_geo = ip_address + try: + ip_for_geo = socket.gethostbyname(ip_address) + except OSError as e: + logger.debug("Could not resolve {}: {}", ip_address, e) + + if _is_private_or_loopback(ip_for_geo): + lat, lon = _default_lab_coords() + else: + geo = _lookup_geo_ipapi(ip_for_geo) + if geo: + lat, lon, _name = geo + name = name or _name + else: + lat, lon = _default_lab_coords() + + loc_factory = TableFactory("locations", session=session) + loc = None + if name: + loc = loc_factory.find_existing({"name": name}) + + if loc is None: + if any(x is None for x in [lat, lon, name]): + logger.warning( + "Skipping location creation for {}: insufficient location data (name={!r}, lat={!r}, lon={!r})", + ip_address, + name, + lat, + lon, + ) + return None + + loc = loc_factory.write( + {"name": name, "lat": lat, "lon": lon, "public": True}, + if_exists="ignore", + ) + + if loc: + return loc.uuid + return None diff --git a/opensampl/load/data.py b/opensampl/load/data.py index 591fb5b..18d8ab2 100644 --- a/opensampl/load/data.py +++ b/opensampl/load/data.py @@ -1,6 +1,6 @@ """Data Factory for defining the unique probe/metric/reference combination to use for Data readings""" -from typing import Any, Optional +from typing import Any from loguru import logger from sqlalchemy.inspection import inspect @@ -25,11 +25,11 @@ class DataFactory: object will also have the database object for any compound references filled as well. """ - probe: Optional[DBProbe] = None - metric: Optional[DBMetricType] = None - db_ref_type: Optional[DBReferenceType] = None - reference: Optional[DBReference] = None - db_compound_reference: Optional[Base] = None + probe: DBProbe | None = None + metric: DBMetricType | None = None + db_ref_type: DBReferenceType | None = None + reference: DBReference | None = None + db_compound_reference: Base | None = None def __init__( self, @@ -37,7 +37,7 @@ def __init__( metric_type: MetricType, reference_type: ReferenceType, session: Session, - compound_key: Optional[dict[str, Any]] = None, + compound_key: dict[str, Any] | None = None, strict: bool = True, ): """ diff --git a/opensampl/load/routing.py b/opensampl/load/routing.py index c1205df..2e602d5 100644 --- a/opensampl/load/routing.py +++ b/opensampl/load/routing.py @@ -1,8 +1,9 @@ """Decorator which ensures we are routing our db operations through a backend if configured, or directly if not.""" import json +from collections.abc import Callable from functools import wraps -from typing import Callable, Literal, Optional +from typing import Literal import requests import requests.exceptions @@ -45,7 +46,7 @@ def decorator(func: Callable) -> Callable: """ @wraps(func) - def wrapper(*args: list, **kwargs: dict) -> Optional[Callable]: + def wrapper(*args: list, **kwargs: dict) -> Callable | None: """ Handle the actual routing logic. diff --git a/opensampl/load/table_factory.py b/opensampl/load/table_factory.py index 0f4a791..f2d9002 100644 --- a/opensampl/load/table_factory.py +++ b/opensampl/load/table_factory.py @@ -1,6 +1,6 @@ """Database table factory for handling CRUD operations with conflict resolution.""" -from typing import Any, Literal, Optional, Union +from typing import Any, Literal from loguru import logger from sqlalchemy import UniqueConstraint, and_, inspect, select @@ -89,7 +89,7 @@ def create_col_filter(self, data: dict[str, Any], cols: list[str]): logger.debug(f"some or all columns from {cols} missing in data") return None - def print_filter_debug(self, filter_expr: Optional[Union[BinaryExpression, BooleanClauseList]], label: str): + def print_filter_debug(self, filter_expr: BinaryExpression | BooleanClauseList | None, label: str): """ Print debug information for a filter expression. @@ -102,7 +102,7 @@ def print_filter_debug(self, filter_expr: Optional[Union[BinaryExpression, Boole compiled = filter_expr.compile(dialect=postgresql.dialect(), compile_kwargs={"literal_binds": True}) logger.debug(f"{label}: {compiled}") - def find_existing(self, data: dict[str, Any]) -> Optional[Base]: + def find_existing(self, data: dict[str, Any]) -> Base | None: """ Find an existing record that matches the provided data. diff --git a/opensampl/load_data.py b/opensampl/load_data.py index f427167..d21f5ce 100644 --- a/opensampl/load_data.py +++ b/opensampl/load_data.py @@ -1,7 +1,7 @@ """Main functionality for loading data into the database""" import json -from typing import Any, Literal, Optional +from typing import Any, Literal import pandas as pd from loguru import logger @@ -10,6 +10,7 @@ from opensampl.config.base import BaseConfig from opensampl.db.orm import Base, ProbeData +from opensampl.helpers.geolocator import create_location from opensampl.load.routing import route from opensampl.load.table_factory import TableFactory from opensampl.metrics import MetricType @@ -25,7 +26,7 @@ def write_to_table( data: dict[str, Any], _config: BaseConfig, if_exists: conflict_actions = "update", - session: Optional[Session] = None, + session: Session | None = None, ): """ Write object to table with configurable behavior for handling conflicts. @@ -79,9 +80,9 @@ def load_time_data( reference_type: ReferenceType, data: pd.DataFrame, _config: BaseConfig, - compound_key: Optional[dict[str, Any]] = None, + compound_key: dict[str, Any] | None = None, strict: bool = True, - session: Optional[Session] = None, + session: Session | None = None, ): """ Write time data to probe_data table @@ -125,9 +126,9 @@ def load_time_data( strict=strict, session=session, ) + probe = data_definition.probe # ty: ignore[possibly-unbound-attribute] probe_readable = ( - data_definition.probe.name # ty: ignore[possibly-unbound-attribute] - or f"{data_definition.probe.ip_address} ({data_definition.probe.probe_id})" # ty: ignore[possibly-unbound-attribute] + probe.name or f"{probe.ip_address} ({probe.probe_id})" # ty: ignore[possibly-unbound-attribute] ) if any(x is None for x in [data_definition.probe, data_definition.metric, data_definition.reference]): @@ -156,11 +157,13 @@ def load_time_data( total_rows = len(records) inserted = result.rowcount # ty: ignore[unresolved-attribute] excluded = total_rows - inserted - - logger.warning( - f"Inserted {inserted}/{total_rows} rows for {probe_readable}; " - f"{excluded}/{total_rows} rejected due to conflicts" - ) + if excluded > 0: + logger.warning( + f"Inserted {inserted}/{total_rows} rows for {probe_readable}; " + f"{excluded}/{total_rows} rejected due to conflicts" + ) + else: + logger.info(f"Inserted {inserted}/{total_rows} rows for {probe_readable}") except Exception as e: # In case of an error, roll back the session @@ -181,7 +184,7 @@ def load_probe_metadata( probe_key: ProbeKey, data: dict[str, Any], _config: BaseConfig, - session: Optional[Session] = None, + session: Session | None = None, ): """Write object to table""" if _config.ROUTE_TO_BACKEND: @@ -199,6 +202,19 @@ def load_probe_metadata( pm_cols = {col.name for col in pm_factory.inspector.columns} probe_info = {k: data.pop(k) for k in list(data.keys()) if k in pm_cols} + location_name = probe_info.pop("location_name", None) + geolocation = ({"name": location_name} if location_name else {}) | probe_info.pop("geolocation", {}) + + if geolocation or _config.ENABLE_GEOLOCATE: + location_uuid = create_location( + session, + geolocate_enabled=_config.ENABLE_GEOLOCATE, + geo_override=geolocation, + ip_address=probe_key.ip_address, + ) + if location_uuid: + probe_info.update({"location_uuid": location_uuid}) + probe_info.update({"probe_id": probe_key.probe_id, "ip_address": probe_key.ip_address, "vendor": vendor.name}) probe = pm_factory.write(data=probe_info, if_exists="update") @@ -214,7 +230,7 @@ def load_probe_metadata( @route("create_new_tables", method="GET") -def create_new_tables(*, _config: BaseConfig, create_schema: bool = True, session: Optional[Session] = None): +def create_new_tables(*, _config: BaseConfig, create_schema: bool = True, session: Session | None = None): """Use the ORM definition to create all tables, optionally creating the schema as well""" if _config.ROUTE_TO_BACKEND: return {"create_schema": create_schema} @@ -227,6 +243,7 @@ def create_new_tables(*, _config: BaseConfig, create_schema: bool = True, sessio session.execute(text(f"CREATE SCHEMA IF NOT EXISTS {Base.metadata.schema}")) session.commit() Base.metadata.create_all(session.bind) + session.commit() except Exception as e: session.rollback() logger.error(f"Error writing to table: {e}") diff --git a/opensampl/metrics.py b/opensampl/metrics.py index 8cc1418..27b6b8a 100644 --- a/opensampl/metrics.py +++ b/opensampl/metrics.py @@ -62,5 +62,70 @@ class METRICS: unit="unknown", value_type=object, ) + DELAY = MetricType( + name="Delay", + description=( + "Round-trip delay (RTD) or Round-Trip Time (RTT). The time in seconds it takes for a data signal to " + "travel from a source to a destination and back, including acknowledgement." + ), + unit="s", + value_type=float, + ) + JITTER = MetricType( + name="Jitter", + description=("Jitter or offset variation in delay in seconds. Represents inconsistent response times."), + unit="s", + value_type=float, + ) + STRATUM = MetricType( + name="Stratum", + description=( + 'Stratum level. Hierarchical layer defining the distance (or "hops") between device and reference.' + ), + unit="level", + value_type=int, + ) + REACHABILITY = MetricType( + name="Reachability", + description=( + "Reachability register (0-255) as a scalar for plotting. Ability of a source node to communicate " + "with a target node." + ), + unit="count", + value_type=float, + ) + DISPERSION = MetricType( + name="Dispersion", + description="Uncertainty in a clock's time relative to its reference source in seconds", + unit="s", + value_type=float, + ) + NTP_ROOT_DELAY = MetricType( + name="NTP Root Delay", + description=( + "Total round-trip network delay from the local system" + " all the way to the primary reference clock (stratum 0)" + ), + unit="s", + value_type=float, + ) + NTP_ROOT_DISPERSION = MetricType( + name="NTP Root Dispersion", + description="The total accumulated clock uncertainty from the local system back to the primary reference clock", + unit="s", + value_type=float, + ) + POLL_INTERVAL = MetricType( + name="Poll Interval", + description="Time between requests sent to a time server in seconds", + unit="s", + value_type=float, + ) + SYNC_HEALTH = MetricType( + name="Sync Health", + description="1.0 if synchronized/healthy, 0.0 otherwise (probe-defined)", + unit="ratio", + value_type=float, + ) # --- CUSTOM METRICS --- !! Do not remove line, used as reference when inserting metric diff --git a/opensampl/mixins/collect.py b/opensampl/mixins/collect.py index 91da293..c7b93d6 100644 --- a/opensampl/mixins/collect.py +++ b/opensampl/mixins/collect.py @@ -2,9 +2,10 @@ import json from abc import ABC, abstractmethod +from collections.abc import Callable from datetime import datetime, timezone from pathlib import Path -from typing import Any, Callable, Optional +from typing import Any import click import pandas as pd @@ -27,15 +28,15 @@ class DataArtifact(BaseModel): value: pd.DataFrame metric: MetricType = METRICS.UNKNOWN reference_type: ReferenceType = REF_TYPES.UNKNOWN - compound_reference: Optional[dict[str, Any]] = None + compound_reference: dict[str, Any] | None = None model_config = ConfigDict(arbitrary_types_allowed=True) class CollectArtifact(BaseModel): """Model for a single probe's collected data""" data: list["CollectMixin.DataArtifact"] - probe_key: Optional[ProbeKey] = None - metadata: Optional[dict] = Field(default_factory=dict) + probe_key: ProbeKey | None = None + metadata: dict | None = Field(default_factory=dict) model_config = ConfigDict(arbitrary_types_allowed=True) @property @@ -58,13 +59,13 @@ class CollectConfig(BaseModel): Attributes: output_dir: When provided, will save collected data as a file to provided directory. - Filename will be automatically generated as {ip_address}_{probe_id}_{vendor}_{timestamp}.txt + Filename will be automatically generated as {vendor}_{ip_address}_{probe_id}_{vendor}_{timestamp}.txt load: Whether to load collected data directly to the database duration: Number of seconds to collect data for """ - output_dir: Optional[Path] = None + output_dir: Path | None = None load: bool = False duration: int = 300 @@ -144,7 +145,7 @@ def _collect_and_save(cls, collect_config: CollectConfig) -> None: @classmethod def filter_files(cls, files: list[Path]) -> list[Path]: """Filter the files found in the input directory when loading this vendor's data files""" - return [f for f in files if f.name.startswith(f"{cls.vendor.parser_class}_") and f.stem == ".txt"] + return [f for f in files if f.name.startswith(f"{cls.vendor.parser_class}_") and f.suffix == ".txt"] @classmethod def load_metadata(cls, probe_key: ProbeKey, metadata: dict) -> None: diff --git a/opensampl/mixins/random_data.py b/opensampl/mixins/random_data.py index 3929e85..99c5f40 100644 --- a/opensampl/mixins/random_data.py +++ b/opensampl/mixins/random_data.py @@ -2,9 +2,10 @@ import random from abc import abstractmethod +from collections.abc import Callable from datetime import datetime, timedelta, timezone from pathlib import Path -from typing import Any, Callable, Optional, Union +from typing import Any import click import numpy as np @@ -25,7 +26,7 @@ class RandomDataConfig(BaseModel): # General configuration num_probes: int = 1 duration_hours: float = 1.0 - seed: Optional[int] = None + seed: int | None = None # Time series parameters sample_interval: float = 1 @@ -37,10 +38,10 @@ class RandomDataConfig(BaseModel): outlier_multiplier: float = 10.0 # Start time (computed at runtime if None) - start_time: Optional[datetime] = None + start_time: datetime | None = None - probe_id: Union[str, None] = None - probe_ip: Optional[str] = None + probe_id: str | None = None + probe_ip: str | None = None @classmethod def _generate_random_ip(cls) -> str: @@ -267,7 +268,7 @@ def _extract_random_data_config(cls, kwargs: dict) -> RandomDataConfig: return cls.RandomDataConfig(**kwargs) @classmethod - def _setup_random_seed(cls, seed: Optional[int]) -> None: + def _setup_random_seed(cls, seed: int | None) -> None: """Set up random seed for reproducible data generation.""" if seed is not None: random.seed(seed) diff --git a/opensampl/server/backend/main.py b/opensampl/server/backend/main.py index a53c5a4..87d3783 100644 --- a/opensampl/server/backend/main.py +++ b/opensampl/server/backend/main.py @@ -5,8 +5,9 @@ import os import sys import time +from collections.abc import Callable from datetime import UTC, datetime, timedelta -from typing import Any, Callable, Optional +from typing import Any import pandas as pd import psycopg2 @@ -102,13 +103,23 @@ def get_keys(): return [] -def validate_api_key(api_key: str = Security(api_key_header)): - """Validate provided API key""" - if not USE_API_KEY: - return None # Security is disabled - if api_key not in get_keys(): - raise HTTPException(status_code=403, detail="Invalid or missing API key") - return api_key +def require_api_key(bootstrap: bool = False): + """Return function to validate api key with or without bootstrap.""" + + def validate_api_key(api_key: str = Security(api_key_header)) -> str | None: + """Validate provided API key""" + if not USE_API_KEY: + return None # Security is disabled + + keys = get_keys() + if not keys and bootstrap: + logger.warning("No API keys configured; allowing bootstrap API key generation") + return None + if api_key not in keys: + raise HTTPException(status_code=403, detail="Invalid or missing API key") + return api_key + + return validate_api_key def get_db(): @@ -145,7 +156,7 @@ async def docs_redirect(): @app.get("/setloglevel") -def set_log_level(newloglevel: str, api_key: str = Depends(validate_api_key)): +def set_log_level(newloglevel: str, api_key: str = Depends(require_api_key())): """Change visible log level in backend container""" newloglevel = newloglevel.upper() logger.configure(handlers=[{"sink": sys.stderr, "level": newloglevel}]) @@ -153,7 +164,7 @@ def set_log_level(newloglevel: str, api_key: str = Depends(validate_api_key)): @app.get("/checkloglevel") -def check_log_level(api_key: str = Depends(validate_api_key)): +def check_log_level(api_key: str = Depends(require_api_key())): """Check which log levels are visible in backend container""" logger.debug("Debug test") logger.info("Info test") @@ -165,7 +176,7 @@ def check_log_level(api_key: str = Depends(validate_api_key)): @app.post("/write_to_table") def write_to_table( - payload: WriteTablePayload, api_key: str = Depends(validate_api_key), session: Session = Depends(get_db) + payload: WriteTablePayload, api_key: str = Depends(require_api_key()), session: Session = Depends(get_db) ): """Write given data to specified table""" try: @@ -190,11 +201,11 @@ def write_to_table( @app.post("/load_time_data") async def load_time_data( # noqa: PLR0912, C901 probe_key_str: str = Form(...), - metric_type_str: Optional[str] = Form(None), - reference_type_str: Optional[str] = Form(None), - compound_key_str: Optional[str] = Form(None), + metric_type_str: str | None = Form(None), + reference_type_str: str | None = Form(None), + compound_key_str: str | None = Form(None), file: UploadFile = File(...), - api_key: str = Depends(validate_api_key), + api_key: str = Depends(require_api_key()), session: Session = Depends(get_db), ): """Load provided data for given probe""" @@ -258,7 +269,7 @@ async def load_time_data( # noqa: PLR0912, C901 @app.post("/load_probe_metadata") def load_probe_metadata( - payload: ProbeMetadataPayload, api_key: str = Depends(validate_api_key), session: Session = Depends(get_db) + payload: ProbeMetadataPayload, api_key: str = Depends(require_api_key()), session: Session = Depends(get_db) ): """Load metadata for given probe""" logger.debug(f"Received payload: {payload.model_dump()}") @@ -289,7 +300,7 @@ def load_probe_metadata( @app.get("/create_new_tables") def create_new_tables( - create_schema: bool = True, api_key: str = Depends(validate_api_key), session: Session = Depends(get_db) + create_schema: bool = True, api_key: str = Depends(require_api_key()), session: Session = Depends(get_db) ): """Update DB based on ORM Tables""" try: @@ -307,7 +318,11 @@ def create_new_tables( @app.get("/gen_api_key") -def generate_api_key(expire_after: Optional[int] = None, session: Session = Depends(get_db)): +def generate_api_key( + expire_after: int | None = None, + api_key: str | None = Depends(require_api_key(bootstrap=True)), + session: Session = Depends(get_db), +): """Generate new API key in the database""" try: new_key = APIAccessKey() diff --git a/opensampl/server/grafana/grafana-dashboards/ntp_dash.json b/opensampl/server/grafana/grafana-dashboards/ntp_dash.json new file mode 100644 index 0000000..345fd8c --- /dev/null +++ b/opensampl/server/grafana/grafana-dashboards/ntp_dash.json @@ -0,0 +1,1412 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "NTP reference path: measurements are relative to OpenSAMPL’s configured default reference (UNKNOWN type) unless you add GNSS-backed probes; timing vs GNSS is not implied for these series.", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 0, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 52, + "panels": [], + "title": "All Probes", + "type": "row" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ns" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 1 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.1", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 51, + "refId": "A" + } + ], + "title": "NTP phase offset (Phase Offset metric)", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "jitter": true, + "metric_type_uuid": true, + "probe_uuid": true, + "reference_uuid": true, + "stratum": true, + "sync_health": true + }, + "includeByName": {}, + "indexByName": { + "metric_type_uuid": 5, + "probe_name": 0, + "probe_uuid": 3, + "reference_uuid": 4, + "time": 1, + "value": 2 + }, + "renameByName": { + "probe_name": "" + } + } + }, + { + "id": "groupingToMatrix", + "options": { + "columnField": "probe_name", + "rowField": "time", + "valueField": "phase_offset" + } + }, + { + "id": "prepareTimeSeries", + "options": { + "format": "multi" + } + } + ], + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "description": "Remote single-packet paths use a conservative jitter estimate from delay and root dispersion when peer RMS jitter is unavailable; local chrony/ntpq snapshots may supply measured jitter.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ns" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.1", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 51, + "refId": "A" + } + ], + "title": "NTP jitter (delay/dispersion estimate or measured)", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "metric_type_uuid": true, + "phase_offset": true, + "probe_uuid": true, + "reference_uuid": true, + "stratum": true, + "sync_health": true + }, + "includeByName": {}, + "indexByName": { + "metric_type_uuid": 5, + "probe_name": 0, + "probe_uuid": 3, + "reference_uuid": 4, + "time": 1, + "value": 2 + }, + "renameByName": {} + } + }, + { + "id": "groupingToMatrix", + "options": { + "columnField": "probe_name", + "emptyValue": "null", + "rowField": "time", + "valueField": "jitter" + } + }, + { + "id": "prepareTimeSeries", + "options": { + "format": "multi" + } + } + ], + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 10 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.1", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 51, + "refId": "A" + } + ], + "title": "NTP stratum", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "jitter": true, + "phase_offset": true, + "probe_uuid": true, + "sync_health": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": {} + } + }, + { + "id": "groupingToMatrix", + "options": { + "columnField": "probe_name", + "rowField": "time", + "valueField": "stratum" + } + }, + { + "id": "prepareTimeSeries", + "options": { + "format": "multi" + } + } + ], + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 10 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.1", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 51, + "refId": "A" + } + ], + "title": "NTP sync health (1=healthy)", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "jitter": true, + "phase_offset": true, + "probe_name": false, + "probe_uuid": true, + "stratum": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": {} + } + }, + { + "id": "groupingToMatrix", + "options": { + "columnField": "probe_name", + "rowField": "time", + "valueField": "sync_health" + } + }, + { + "id": "prepareTimeSeries", + "options": { + "format": "multi" + } + } + ], + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 18 + }, + "id": 53, + "panels": [], + "repeat": "ntp_reference", + "title": "Reference: $ntp_reference", + "type": "row" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ns" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 19 + }, + "id": 54, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.1", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 51, + "refId": "A" + } + ], + "title": "NTP phase offset (Phase Offset metric)", + "transformations": [ + { + "id": "filterByValue", + "options": { + "filters": [ + { + "config": { + "id": "equal", + "options": { + "value": "${ntp_reference}" + } + }, + "fieldName": "reference_uuid" + } + ], + "match": "all", + "type": "include" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "jitter": true, + "metric_type_uuid": true, + "probe_uuid": true, + "reference_uuid": true, + "stratum": true, + "sync_health": true + }, + "includeByName": {}, + "indexByName": { + "metric_type_uuid": 5, + "probe_name": 0, + "probe_uuid": 3, + "reference_uuid": 4, + "time": 1, + "value": 2 + }, + "renameByName": { + "probe_name": "" + } + } + }, + { + "id": "groupingToMatrix", + "options": { + "columnField": "probe_name", + "rowField": "time", + "valueField": "phase_offset" + } + }, + { + "id": "prepareTimeSeries", + "options": { + "format": "multi" + } + } + ], + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "description": "Remote single-packet paths use a conservative jitter estimate from delay and root dispersion when peer RMS jitter is unavailable; local chrony/ntpq snapshots may supply measured jitter.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ns" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 19 + }, + "id": 55, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.1", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 51, + "refId": "A" + } + ], + "title": "NTP jitter (delay/dispersion estimate or measured)", + "transformations": [ + { + "id": "filterByValue", + "options": { + "filters": [ + { + "config": { + "id": "equal", + "options": { + "value": "${ntp_reference}" + } + }, + "fieldName": "reference_uuid" + } + ], + "match": "all", + "type": "include" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "metric_type_uuid": true, + "phase_offset": true, + "probe_uuid": true, + "reference_uuid": true, + "stratum": true, + "sync_health": true + }, + "includeByName": {}, + "indexByName": { + "metric_type_uuid": 5, + "probe_name": 0, + "probe_uuid": 3, + "reference_uuid": 4, + "time": 1, + "value": 2 + }, + "renameByName": {} + } + }, + { + "id": "groupingToMatrix", + "options": { + "columnField": "probe_name", + "emptyValue": "null", + "rowField": "time", + "valueField": "jitter" + } + }, + { + "id": "prepareTimeSeries", + "options": { + "format": "multi" + } + } + ], + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 27 + }, + "id": 56, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.1", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 51, + "refId": "A" + } + ], + "title": "NTP stratum", + "transformations": [ + { + "id": "filterByValue", + "options": { + "filters": [ + { + "config": { + "id": "equal", + "options": { + "value": "${ntp_reference}" + } + }, + "fieldName": "reference_uuid" + } + ], + "match": "all", + "type": "include" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "jitter": true, + "phase_offset": true, + "probe_uuid": true, + "sync_health": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": {} + } + }, + { + "id": "groupingToMatrix", + "options": { + "columnField": "probe_name", + "rowField": "time", + "valueField": "stratum" + } + }, + { + "id": "prepareTimeSeries", + "options": { + "format": "multi" + } + } + ], + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 27 + }, + "id": 57, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.1", + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 51, + "refId": "A" + } + ], + "title": "NTP sync health (1=healthy)", + "transformations": [ + { + "id": "filterByValue", + "options": { + "filters": [ + { + "config": { + "id": "equal", + "options": { + "value": "${ntp_reference}" + } + }, + "fieldName": "reference_uuid" + } + ], + "match": "all", + "type": "include" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "jitter": true, + "phase_offset": true, + "probe_name": false, + "probe_uuid": true, + "stratum": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": {} + } + }, + { + "id": "groupingToMatrix", + "options": { + "columnField": "probe_name", + "rowField": "time", + "valueField": "sync_health" + } + }, + { + "id": "prepareTimeSeries", + "options": { + "format": "multi" + } + } + ], + "type": "timeseries" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 52 + }, + "id": 50, + "panels": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "castdb-datasource" + }, + "description": "Phase metrics use OpenSAMPL’s default reference row (UNKNOWN reference type). NTP **observation** context is the configured server in `ntp_metadata` (not GNSS unless a GNSS-backed probe is present).", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 87 + }, + "id": 5, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [ + { + "desc": false, + "displayName": "probe" + } + ] + }, + "pluginVersion": "12.2.1", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "castdb-datasource" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\n COALESCE(pm.name, CONCAT(pm.ip_address, ' ', pm.probe_id)) AS probe,\n pm.vendor,\n COALESCE(rt.name, '') AS reference_type,\n COALESCE(nm.target_host::text, '') AS ntp_server,\n COALESCE(nm.mode::text, '') AS ntp_mode,\n COALESCE(nm.reference_id::text, '') AS ntp_ref_id,\n COALESCE(l.name, '') AS location,\n COALESCE(pm.public::text, '') AS public\nFROM castdb.probe_metadata pm\nLEFT JOIN castdb.ntp_metadata nm ON nm.probe_uuid = pm.uuid\nLEFT JOIN castdb.locations l ON l.uuid = pm.location_uuid\nLEFT JOIN LATERAL (\n SELECT pd.reference_uuid FROM castdb.probe_data pd WHERE pd.probe_uuid = pm.uuid LIMIT 1\n) rp ON true\nLEFT JOIN castdb.reference r ON r.uuid = rp.reference_uuid\nLEFT JOIN castdb.reference_type rt ON rt.uuid = r.reference_type_uuid\nWHERE pm.vendor = 'NTP'\n AND (trim('${ntp_probe:csv}') = '' OR pm.uuid = ANY(string_to_array(trim('${ntp_probe:csv}'), ',')))\nORDER BY 1;", + "refId": "A" + } + ], + "title": "Probe reference & source (stored metadata)", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "P55EB97F79F5EB88E" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "hideFrom": { + "viz": false + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 96 + }, + "id": 51, + "options": { + "cellHeight": "sm", + "frameIndex": 1, + "showHeader": true + }, + "pluginVersion": "12.2.1", + "targets": [ + { + "dataset": "castdb", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "WITH probe_ref AS (\n SELECT\n uuid,\n COALESCE(pm.name, CONCAT(pm.ip_address, ' ', pm.probe_id)) AS probe_name\n FROM castdb.probe_metadata pm\n)\nSELECT\n time_bucket('1 minute'::interval, pd.time AT TIME ZONE 'UTC') AS time,\n pd.probe_uuid,\n pr.probe_name,\n pd.reference_uuid,\n AVG(pd.value::float * 1e9) FILTER (WHERE lower(m.name) = 'phase offset') AS phase_offset,\n AVG(pd.value::float * 1e9) FILTER (WHERE lower(m.name) = 'jitter') AS jitter,\n AVG((pd.value)::float) FILTER (WHERE lower(m.name) = 'stratum') AS stratum,\n AVG((pd.value)::float) FILTER (WHERE lower(m.name) = 'sync health') AS sync_health\nFROM castdb.probe_data pd\nJOIN probe_ref pr\n ON pd.probe_uuid = pr.uuid\nJOIN castdb.metric_type m\n ON pd.metric_type_uuid = m.uuid\nWHERE pd.probe_uuid = ANY(ARRAY[${ntp_probe:sqlstring}]::text[]) AND $__timeFilter(pd.time)\nGROUP BY 1, 2, 3, 4\nORDER BY\n 1, 3;", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "source_panel", + "type": "table" + } + ], + "title": "Reference & source metadata", + "type": "row" + } + ], + "preload": false, + "refresh": "30s", + "schemaVersion": 42, + "tags": [ + "ntp", + "opensampl", + "reference" + ], + "templating": { + "list": [ + { + "current": { + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "castdb-datasource" + }, + "definition": "SELECT pm.uuid::text AS __value, COALESCE(pm.name, CONCAT(pm.ip_address, ' ', pm.probe_id)) AS __text FROM castdb.probe_metadata pm WHERE pm.vendor = 'NTP' ORDER BY 2", + "includeAll": true, + "multi": true, + "name": "ntp_probe", + "options": [], + "query": "SELECT pm.uuid::text AS __value, COALESCE(pm.name, CONCAT(pm.ip_address, ' ', pm.probe_id)) AS __text FROM castdb.probe_metadata pm WHERE pm.vendor = 'NTP' ORDER BY 2", + "refresh": 1, + "regex": "", + "sort": 1, + "type": "query" + }, + { + "current": { + "text": "All", + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "castdb-datasource" + }, + "definition": "SELECT pm.reference_uuid::text AS __value, COALESCE(pm.name, CONCAT(pm.ip_address, ' ', pm.probe_id)) AS __text FROM castdb.reference_probe_metadata pm WHERE pm.vendor = 'NTP' \nUNION ALL\nSELECT r.uuid::text AS __value, rt.\"name\" AS __text FROM castdb.reference r JOIN castdb.reference_type rt ON r.reference_type_uuid = rt.\"uuid\" WHERE rt.\"name\" = 'UNKNOWN';", + "includeAll": true, + "multi": true, + "name": "ntp_reference", + "options": [], + "query": "SELECT pm.reference_uuid::text AS __value, COALESCE(pm.name, CONCAT(pm.ip_address, ' ', pm.probe_id)) AS __text FROM castdb.reference_probe_metadata pm WHERE pm.vendor = 'NTP' \nUNION ALL\nSELECT r.uuid::text AS __value, rt.\"name\" AS __text FROM castdb.reference r JOIN castdb.reference_type rt ON r.reference_type_uuid = rt.\"uuid\" WHERE rt.\"name\" = 'UNKNOWN';", + "refresh": 1, + "regex": "", + "sort": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "NTP probes (NTP server reference path)", + "uid": "ntp-opensampl", + "version": 17 +} \ No newline at end of file diff --git a/opensampl/server/grafana/grafana-dashboards/public-timing-dashboard.json b/opensampl/server/grafana/grafana-dashboards/public-timing-dashboard.json index 687ceae..24b7a6f 100644 --- a/opensampl/server/grafana/grafana-dashboards/public-timing-dashboard.json +++ b/opensampl/server/grafana/grafana-dashboards/public-timing-dashboard.json @@ -338,7 +338,7 @@ "group": [], "metricColumn": "none", "rawQuery": true, - "rawSql": "select\n pm.name as \"Clock\",\n l.name as \"Location Name\",\n l.latitude,\n l.longitude,\n l.campus\nfrom castdb.campus_locations l left join castdb.probe_metadata pm on l.uuid = pm.location_uuid\nwhere pm.uuid in (${clock_name:sqlstring});", + "rawSql": "select\n pm.name as \"Clock\",\n l.name as \"Location Name\",\n l.latitude,\n l.longitude,\n l.campus\nfrom castdb.campus_locations l left join castdb.probe_metadata pm on l.uuid = pm.location_uuid\nwhere (trim('${clock_name:csv}') = '' OR pm.uuid = ANY(string_to_array(trim('${clock_name:csv}'), ',')));", "refId": "ClockProbes", "select": [ [ @@ -379,13 +379,13 @@ { "datasource": { "type": "grafana-postgresql-datasource", - "uid": "P55EB97F79F5EB88E" + "uid": "castdb-datasource" }, "editorMode": "code", "format": "table", "hide": false, "rawQuery": true, - "rawSql": "SELECT\n l.latitude,\n l.longitude,\n l.campus,\n sum(\n CASE\n when pm.public = True and pm.vendor in ('ADVA', 'MicrochipTP4100') then 1 else 0\n end \n ) as visible_clocks,\n sum(\n CASE\n when pm.uuid in (${clock_name:sqlstring}) then 1 else 0\n end \n ) as selected_clocks\n from castdb.campus_locations l left join castdb.probe_metadata pm on l.uuid = pm.location_uuid\n where l.public = True\n group by\n l.latitude, l.longitude, l.campus;", + "rawSql": "SELECT\n l.latitude,\n l.longitude,\n l.campus,\n sum(\n CASE\n when pm.public = True and pm.vendor in ('ADVA', 'MicrochipTP4100', 'NTP') then 1 else 0\n end \n ) as visible_clocks,\n sum(\n CASE\n when (trim('${clock_name:csv}') = '' OR pm.uuid = ANY(string_to_array(trim('${clock_name:csv}'), ','))) then 1 else 0\n end \n ) as selected_clocks\n from castdb.campus_locations l left join castdb.probe_metadata pm on l.uuid = pm.location_uuid\n where l.public = True\n group by\n l.latitude, l.longitude, l.campus;", "refId": "A", "sql": { "columns": [ @@ -465,7 +465,7 @@ }, "format": "table", "rawQuery": true, - "rawSql": "SELECT COUNT(*) as \"Total Clock Probes\" FROM castdb.probe_metadata where uuid in ($clock_name)", + "rawSql": "SELECT COUNT(*)::bigint AS \"Total Clock Probes\" FROM castdb.probe_metadata pm WHERE pm.vendor IN ('ADVA', 'MicrochipTP4100', 'NTP') AND coalesce(pm.public, true) AND (trim('${clock_name:csv}') = '' OR pm.uuid = ANY(string_to_array(trim('${clock_name:csv}'), ',')))", "refId": "A" } ], @@ -539,7 +539,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "SELECT \n coalesce(pm.name, concat(pm.ip_address, 'Inteface', pm.probe_id)) as \"Clock Probe\", \n COUNT(*) as \"Total Records\" \nFROM castdb.probe_data pd\njoin castdb.probe_metadata pm on pd.probe_uuid = pm.uuid \nwhere pm.uuid in (${clock_name:sqlstring})\nAND pd.\"time\" >= $__timeFrom()\nAND pd.\"time\" <= $__timeTo()\ngroup by pm.uuid, pm.name, pm.ip_address, pm.probe_id;", + "rawSql": "SELECT \n coalesce(pm.name, concat(pm.ip_address, ' Interface ', pm.probe_id)) AS \"Clock Probe\", \n COUNT(*)::bigint AS \"Total Records\" \nFROM castdb.probe_data pd\nJOIN castdb.probe_metadata pm ON pd.probe_uuid = pm.uuid \nWHERE pm.vendor IN ('ADVA', 'MicrochipTP4100', 'NTP')\n AND coalesce(pm.public, true)\n AND (trim('${clock_name:csv}') = '' OR pm.uuid = ANY(string_to_array(trim('${clock_name:csv}'), ',')))\n AND pd.\"time\" >= $__timeFrom()\n AND pd.\"time\" <= $__timeTo()\nGROUP BY pm.uuid, pm.name, pm.ip_address, pm.probe_id;", "refId": "A", "sql": { "columns": [ @@ -568,7 +568,7 @@ "type": "grafana-postgresql-datasource", "uid": "castdb-datasource" }, - "description": "Average time error \n(averaged on selected resolution)", + "description": "Average time error vs stored reference (resolution as selected). GNSS-specific labeling applies only when the probe/reference model is GNSS-backed.", "fieldConfig": { "defaults": { "color": { @@ -650,7 +650,7 @@ "editorMode": "code", "format": "time_series", "rawQuery": true, - "rawSql": "SELECT \n time_bucket(${resolution:sqlstring}, pd.time AT TIME ZONE 'UTC') AS time,\n coalesce(pm.name, concat(pm.ip_address, ' Interface ', pm.probe_id)),\n AVG(pd.value::FLOAT) * 1e9 AS value\nFROM castdb.probe_data pd join castdb.probe_metadata pm on pd.probe_uuid = pm.uuid\nWHERE\n $__timeFilter(pd.time)\n AND pd.probe_uuid IN (${clock_name:sqlstring})\nGROUP BY \n time_bucket(${resolution:sqlstring}, pd.time AT TIME ZONE 'UTC'),\n pd.probe_uuid,\n pm.name,\n pm.ip_address,\n pm.probe_id\nORDER BY \n time_bucket(${resolution:sqlstring}, pd.time AT TIME ZONE 'UTC')\n", + "rawSql": "SELECT \n time_bucket(${resolution:sqlstring}, pd.time AT TIME ZONE 'UTC') AS time,\n coalesce(pm.name, concat(pm.ip_address, ' Interface ', pm.probe_id)),\n AVG(pd.value::FLOAT) * 1e9 AS value\nFROM castdb.probe_data pd JOIN castdb.probe_metadata pm ON pd.probe_uuid = pm.uuid\nWHERE\n $__timeFilter(pd.time)\n AND pm.vendor IN ('ADVA', 'MicrochipTP4100', 'NTP')\n AND coalesce(pm.public, true)\n AND (trim('${clock_name:csv}') = '' OR pd.probe_uuid = ANY(string_to_array(trim('${clock_name:csv}'), ',')))\nGROUP BY \n time_bucket(${resolution:sqlstring}, pd.time AT TIME ZONE 'UTC'),\n pd.probe_uuid,\n pm.name,\n pm.ip_address,\n pm.probe_id\nORDER BY \n time_bucket(${resolution:sqlstring}, pd.time AT TIME ZONE 'UTC')\n", "refId": "A", "sql": { "columns": [ @@ -671,7 +671,7 @@ } } ], - "title": "Time Error - Clock Time vs GNSS", + "title": "Time Error - Clock Time vs Reference", "transformations": [ { "id": "prepareTimeSeries", @@ -776,7 +776,7 @@ "editorMode": "code", "format": "time_series", "rawQuery": true, - "rawSql": "SELECT \n time_bucket(${resolution:sqlstring}, pd.time AT TIME ZONE 'UTC') AS time,\n COALESCE(name, CONCAT(ip_address, ' Interface ', probe_id)),\n (MAX(pd.value::FLOAT) - MIN(pd.value::FLOAT)) * 1e9 AS value\nFROM castdb.probe_data pd join castdb.probe_metadata pm on pd.probe_uuid = pm.uuid\nWHERE\n $__timeFilter(pd.time)\n AND pd.probe_uuid in (${clock_name:sqlstring})\nGROUP BY \n time_bucket(${resolution:sqlstring}, pd.time AT TIME ZONE 'UTC'),\n pd.probe_uuid,\n pm.name,\n pm.ip_address,\n pm.probe_id\nORDER BY \n time_bucket(${resolution:sqlstring}, pd.time AT TIME ZONE 'UTC')\n", + "rawSql": "SELECT \n time_bucket(${resolution:sqlstring}, pd.time AT TIME ZONE 'UTC') AS time,\n COALESCE(pm.name, CONCAT(pm.ip_address, ' Interface ', pm.probe_id)),\n (MAX(pd.value::FLOAT) - MIN(pd.value::FLOAT)) * 1e9 AS value\nFROM castdb.probe_data pd JOIN castdb.probe_metadata pm ON pd.probe_uuid = pm.uuid\nWHERE\n $__timeFilter(pd.time)\n AND pm.vendor IN ('ADVA', 'MicrochipTP4100', 'NTP')\n AND coalesce(pm.public, true)\n AND (trim('${clock_name:csv}') = '' OR pd.probe_uuid = ANY(string_to_array(trim('${clock_name:csv}'), ',')))\nGROUP BY \n time_bucket(${resolution:sqlstring}, pd.time AT TIME ZONE 'UTC'),\n pd.probe_uuid,\n pm.name,\n pm.ip_address,\n pm.probe_id\nORDER BY \n time_bucket(${resolution:sqlstring}, pd.time AT TIME ZONE 'UTC')\n", "refId": "A", "sql": { "columns": [ @@ -797,7 +797,7 @@ } } ], - "title": "Maximum Time Interval Error VS GNSS", + "title": "Maximum Time Interval Error vs Reference", "transformations": [ { "id": "prepareTimeSeries", @@ -834,7 +834,7 @@ "type": "grafana-postgresql-datasource", "uid": "castdb-datasource" }, - "description": "Average time error \n(averaged on selected resolution)", + "description": "Average time error vs stored reference (resolution as selected). GNSS-specific labeling applies only when the probe/reference model is GNSS-backed.", "fieldConfig": { "defaults": { "color": { @@ -937,7 +937,7 @@ } } ], - "title": "Time Error - Clock Time vs GNSS", + "title": "Time Error - Clock Time vs Reference", "type": "timeseries" }, { @@ -1048,14 +1048,44 @@ } } ], - "title": "Maximum Time Interval Error", + "title": "Maximum Time Interval Error vs Reference", "type": "timeseries" + }, + { + "collapsed": true, + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 29}, + "id": 101, + "panels": [ + { + "datasource": {"type": "grafana-postgresql-datasource", "uid": "castdb-datasource"}, + "description": "Rows reflect stored `probe_metadata`, `ntp_metadata` (when vendor is NTP), `locations`, and one sample `reference`/`reference_type` from `probe_data` per probe.", + "fieldConfig": {"defaults": {}, "overrides": []}, + "gridPos": {"h": 10, "w": 24, "x": 0, "y": 0}, + "id": 100, + "options": {"cellHeight": "sm", "showHeader": true}, + "pluginVersion": "12.0.0", + "targets": [ + { + "datasource": {"type": "grafana-postgresql-datasource", "uid": "castdb-datasource"}, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\n COALESCE(pm.name, CONCAT(pm.ip_address, ' ', pm.probe_id)) AS probe,\n pm.vendor,\n COALESCE(rt.name, '') AS reference_type,\n COALESCE(nm.target_host::text, '') AS ntp_server,\n COALESCE(nm.mode::text, '') AS ntp_mode,\n COALESCE(nm.reference_id::text, '') AS ntp_ref_id,\n COALESCE(l.name, '') AS location,\n COALESCE(pm.public::text, '') AS public\nFROM castdb.probe_metadata pm\nLEFT JOIN castdb.ntp_metadata nm ON nm.probe_uuid = pm.uuid\nLEFT JOIN castdb.locations l ON l.uuid = pm.location_uuid\nLEFT JOIN LATERAL (\n SELECT pd.reference_uuid FROM castdb.probe_data pd WHERE pd.probe_uuid = pm.uuid LIMIT 1\n) rp ON true\nLEFT JOIN castdb.reference r ON r.uuid = rp.reference_uuid\nLEFT JOIN castdb.reference_type rt ON rt.uuid = r.reference_type_uuid\nWHERE pm.vendor IN ('ADVA', 'MicrochipTP4100', 'NTP')\n AND coalesce(pm.public, true)\n AND (trim('${clock_name:csv}') = '' OR pm.uuid = ANY(string_to_array(trim('${clock_name:csv}'), ',')))\nORDER BY 1;", + "refId": "A" + } + ], + "title": "Probe reference & source (stored metadata)", + "type": "table" + } + ], + "title": "Reference & source metadata", + "type": "row" } ], "preload": false, "refresh": "", "schemaVersion": 41, - "tags": [], + "tags": ["opensampl", "reference", "geospatial"], "templating": { "list": [ { @@ -1067,12 +1097,12 @@ "type": "grafana-postgresql-datasource", "uid": "castdb-datasource" }, - "definition": "SELECT uuid AS __value, COALESCE(name, CONCAT(ip_address, ' Interface ', probe_id)) AS __text FROM castdb.probe_metadata WHERE vendor in ('ADVA', 'MicrochipTP4100') and public;", + "definition": "SELECT pm.uuid::text AS __value, COALESCE(pm.name, CONCAT(pm.ip_address, ' Interface ', pm.probe_id)) AS __text FROM castdb.probe_metadata pm WHERE pm.vendor in ('ADVA', 'MicrochipTP4100', 'NTP') AND coalesce(pm.public, true) ORDER BY 2;", "includeAll": true, "multi": true, "name": "clock_name", "options": [], - "query": "SELECT uuid AS __value, COALESCE(name, CONCAT(ip_address, ' Interface ', probe_id)) AS __text FROM castdb.probe_metadata WHERE vendor in ('ADVA', 'MicrochipTP4100') and public;", + "query": "SELECT pm.uuid::text AS __value, COALESCE(pm.name, CONCAT(pm.ip_address, ' Interface ', pm.probe_id)) AS __text FROM castdb.probe_metadata pm WHERE pm.vendor in ('ADVA', 'MicrochipTP4100', 'NTP') AND coalesce(pm.public, true) ORDER BY 2;", "refresh": 1, "regex": "", "type": "query" @@ -1130,7 +1160,8 @@ }, "timepicker": {}, "timezone": "utc", - "title": "Public Geospatial and Timing Combined Dashboard", + "description": "Geospatial views use stored `locations` geometry. Timing series are relative to each probe\u2019s stored reference (OpenSAMPL `reference` / `reference_type`) and are **not** GNSS-truth unless a GNSS-backed probe supplies that semantics.", + "title": "Public Geospatial and Timing (Reference)", "uid": "public-geospatial-dashboard", "version": 10 } \ No newline at end of file diff --git a/opensampl/server/migrations/_migrations/versions/2026_04_17_1243_add_ntp_values.py b/opensampl/server/migrations/_migrations/versions/2026_04_17_1243_add_ntp_values.py new file mode 100644 index 0000000..4cd6b14 --- /dev/null +++ b/opensampl/server/migrations/_migrations/versions/2026_04_17_1243_add_ntp_values.py @@ -0,0 +1,159 @@ +"""add ntp values + +Revision ID: 5665e5902905 +Revises: d419cac01df2 +Create Date: 2026-04-17 12:43:23.711453 + +""" +from typing import Sequence, Union +import uuid +from sqlalchemy.dialects import postgresql +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '5665e5902905' +down_revision: Union[str, None] = 'd419cac01df2' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + +SCHEMA = 'castdb' + +def upgrade() -> None: + op.create_table( + "ntp_metadata", + sa.Column( + "probe_uuid", + sa.String(), + sa.ForeignKey(f"{SCHEMA}.probe_metadata.uuid"), + primary_key=True, + nullable=False, + ), + sa.Column("mode", sa.Text(), nullable=True), + sa.Column( + "reference", + sa.Boolean(), + nullable=True, + comment="Is used as a reference for other probes", + ), + sa.Column("target_host", sa.Text(), nullable=True), + sa.Column("target_port", sa.Integer(), nullable=True), + sa.Column("sync_status", sa.Text(), nullable=True), + sa.Column("leap_status", sa.Text(), nullable=True), + sa.Column("reference_id", sa.Text(), nullable=True), + sa.Column("observation_sources", postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column("collection_id", sa.Text(), nullable=True), + sa.Column("collection_ip", sa.Text(), nullable=True), + sa.Column("timeout", sa.Float(), nullable=True), + sa.Column("additional_metadata", postgresql.JSONB(astext_type=sa.Text()), nullable=True), + schema=SCHEMA, + if_not_exists=True, + comment="NTP Clock Probe specific metadata" + ) + + metric_type_table = sa.table('metric_type', + sa.column('uuid', sa.String), + sa.column('name', sa.String), + sa.column('description', sa.Text), + sa.column('unit', sa.String), + sa.column('value_type', sa.String), + schema=SCHEMA + ) + new_metrics = [ + dict(uuid=str(uuid.uuid4()), + name="Delay", + description=( + "Round-trip delay (RTD) or Round-Trip Time (RTT). The time in seconds it takes for a data signal to " + "travel from a source to a destination and back, including acknowledgement." + ), + unit="s", + value_type='float', + ), + dict(uuid=str(uuid.uuid4()), + name="Jitter", + description=("Jitter or offset variation in delay in seconds. Represents inconsistent response times."), + unit="s", + value_type='float', + ), + dict(uuid=str(uuid.uuid4()), + name="Stratum", + description=( + 'Stratum level. Hierarchical layer defining the distance (or "hops") between device and reference.' + ), + unit="level", + value_type='int', + ), + dict(uuid=str(uuid.uuid4()), + name="Reachability", + description=( + "Reachability register (0-255) as a scalar for plotting. Ability of a source node to communicate " + "with a target node." + ), + unit="count", + value_type='float', + ), + dict(uuid=str(uuid.uuid4()), + name="Dispersion", + description="Uncertainty in a clock's time relative to its reference source in seconds", + unit="s", + value_type='float', + ), + dict(uuid=str(uuid.uuid4()), + name="NTP Root Delay", + description=( + "Total round-trip network delay from the local system" + " all the way to the primary reference clock (stratum 0)" + ), + unit="s", + value_type='float' + ), + dict(uuid=str(uuid.uuid4()), + name="NTP Root Dispersion", + description="The total accumulated clock uncertainty from the local system back to the primary reference clock", + unit="s", + value_type='float', + ), + dict(uuid=str(uuid.uuid4()), + name="Poll Interval", + description="Time between requests sent to a time server in seconds", + unit="s", + value_type='float', + ), + dict(uuid=str(uuid.uuid4()), + name="Sync Health", + description="1.0 if synchronized/healthy, 0.0 otherwise (probe-defined)", + unit="ratio", + value_type='float', + ) + ] + op.bulk_insert(metric_type_table, new_metrics) + + + + +def downgrade() -> None: + op.drop_table('ntp_metadata', schema=SCHEMA, if_exists=True) + metric_type = sa.sql.table( + "metric_type", + sa.column("name", sa.String), + schema=SCHEMA, + ) + + op.execute( + metric_type.delete().where( + metric_type.c.name.in_( + [ + "Delay", + "Jitter", + "Stratum", + "Reachability", + "Dispersion", + "NTP Root Delay", + "NTP Root Dispersion", + "Poll Interval", + "Sync Health", + ] + ) + ) + ) diff --git a/opensampl/server/migrations/_migrations/versions/2026_04_17_1254_add_reference_view.py b/opensampl/server/migrations/_migrations/versions/2026_04_17_1254_add_reference_view.py new file mode 100644 index 0000000..8cbb326 --- /dev/null +++ b/opensampl/server/migrations/_migrations/versions/2026_04_17_1254_add_reference_view.py @@ -0,0 +1,56 @@ +"""add reference view + +Revision ID: c95e49e551be +Revises: 5665e5902905 +Create Date: 2026-04-17 12:54:27.037125 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'c95e49e551be' +down_revision: Union[str, None] = '5665e5902905' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + +SCHEMA = 'castdb' + +CREATE_VIEW_SQL = f""" +CREATE VIEW {SCHEMA}.reference_probe_metadata +AS WITH probe_references AS ( + SELECT r.uuid, + r.reference_type_uuid, + r.compound_reference_uuid + FROM {SCHEMA}.reference r + JOIN {SCHEMA}.reference_type rt ON r.reference_type_uuid::text = rt.uuid::text + WHERE rt.name::text = 'PROBE'::text + ) + SELECT pm.uuid, + pm.probe_id, + pm.ip_address, + pm.vendor, + pm.model, + pm.name, + pm.public, + pm.location_uuid, + pm.test_uuid, + pr.uuid AS reference_uuid + FROM probe_references pr + JOIN {SCHEMA}.probe_metadata pm ON pr.compound_reference_uuid::text = pm.uuid::text; +""" + +DROP_VIEW_SQL = f""" +DROP VIEW IF EXISTS {SCHEMA}.reference_probe_metadata""" + +def upgrade() -> None: + # Drop the view first, just to be extra safe. + op.execute(DROP_VIEW_SQL) + op.execute(CREATE_VIEW_SQL) + + +def downgrade() -> None: + op.execute(DROP_VIEW_SQL) diff --git a/opensampl/vendors/adva.py b/opensampl/vendors/adva.py index 5ea9808..1d6ed36 100644 --- a/opensampl/vendors/adva.py +++ b/opensampl/vendors/adva.py @@ -5,7 +5,7 @@ import re from datetime import datetime, timezone from pathlib import Path -from typing import ClassVar, TextIO, Union +from typing import ClassVar, TextIO import click import pandas as pd @@ -63,9 +63,9 @@ def get_random_data_cli_options(cls) -> list: ] return base_options + vendor_options - def __init__(self, input_file: Union[str, Path]): + def __init__(self, input_file: str | Path, **kwargs: dict): """Initialize AdvaProbe object give input_file and determines probe identity from filename""" - super().__init__(input_file=input_file) + super().__init__(input_file=input_file, **kwargs) self.probe_key, self.timestamp = self.parse_file_name(self.input_file) @classmethod @@ -95,7 +95,7 @@ def parse_file_name(cls, file_name: Path) -> tuple[ProbeKey, datetime]: return ProbeKey(probe_id=probe_id, ip_address=ip_address), timestamp raise ValueError(f"Could not parse file name {file_name} into probe key and timestamp for ADVA probe") - def _open_file(self) -> Union[TextIO, gzip.GzipFile]: + def _open_file(self) -> TextIO | gzip.GzipFile: """Open the input file, handling both .txt and .txt.gz formats""" if self.input_file.name.endswith(".gz"): return gzip.open(self.input_file, "rt") diff --git a/opensampl/vendors/base_probe.py b/opensampl/vendors/base_probe.py index 1253844..f2188ce 100644 --- a/opensampl/vendors/base_probe.py +++ b/opensampl/vendors/base_probe.py @@ -4,11 +4,12 @@ import shutil from abc import ABC, abstractmethod +from collections.abc import Callable from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager from datetime import datetime, timezone from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, ClassVar, TypeVar +from typing import TYPE_CHECKING, Any, ClassVar, TypeVar import click import psycopg2.errors @@ -461,16 +462,16 @@ def ip_address(self): return self.probe_key.ip_address @abstractmethod - def process_time_data(self) -> pd.DataFrame: + def process_time_data(self) -> None: """ - Process time series data. + Parse and load time series data from self.input_file. - Returns - ------- - pd.DataFrame: DataFrame with columns: + Use either send_time_data (which prefills METRICS.PHASE_OFFSET) + or send_data and provide alternative METRICS type. + Both require a df as follows: + pd.DataFrame with columns: - time (datetime64[ns]): timestamp for each measurement - value (float64): measured value at each timestamp - """ @dualmethod @@ -483,13 +484,13 @@ def send_data( probe_key: ProbeKey | None = None, ) -> None: """Ingests data into the database""" - if isinstance(self, BaseProbe): + if isinstance(self, BaseProbe) and probe_key is None: probe_key = self.probe_key if probe_key is None: raise ValueError("send data must be called with probe_key if used as class method") - if self.chunk_size: + if hasattr(self, "chunk_size") and self.chunk_size: for chunk_start in range(0, len(data), self.chunk_size): chunk = data.iloc[chunk_start : chunk_start + self.chunk_size] load_time_data( diff --git a/opensampl/vendors/constants.py b/opensampl/vendors/constants.py index 4725b05..b5b7bdd 100644 --- a/opensampl/vendors/constants.py +++ b/opensampl/vendors/constants.py @@ -70,6 +70,14 @@ class VENDORS: metadata_orm="MicrochipTP4100Metadata", ) + NTP = VendorType( + name="NTP", + parser_class="NtpProbe", + parser_module="ntp", + metadata_table="ntp_metadata", + metadata_orm="NtpMetadata", + ) + # --- CUSTOM VENDORS --- !! Do not remove line, used as reference when inserting vendor # --- VENDOR FUNCTIONS --- diff --git a/opensampl/vendors/microchip/tp4100.py b/opensampl/vendors/microchip/tp4100.py index 09b0d4d..e6d2f31 100644 --- a/opensampl/vendors/microchip/tp4100.py +++ b/opensampl/vendors/microchip/tp4100.py @@ -2,7 +2,7 @@ import random from pathlib import Path -from typing import ClassVar, Optional, Union +from typing import ClassVar import click import pandas as pd @@ -30,13 +30,13 @@ class RandomDataConfig(RandomDataMixin.RandomDataConfig): """Model for storing random data generation configurations as provided by CLI or YAML""" # Time series parameters - base_value: Optional[float] = Field( + base_value: float | None = Field( default_factory=lambda: random.uniform(-5e-7, 5e-7), description="random.uniform(-5e-7, 5e-7)" ) - noise_amplitude: Optional[float] = Field( + noise_amplitude: float | None = Field( default_factory=lambda: random.uniform(1e-8, 5e-8), description="random.uniform(1e-8, 5e-8)" ) - drift_rate: Optional[float] = Field( + drift_rate: float | None = Field( default_factory=lambda: random.uniform(-1e-10, 1e-10), description="random.uniform(-1e-10, 1e-10)" ) @@ -59,9 +59,9 @@ def get_random_data_cli_options(cls) -> list: ] return base_options + vendor_options - def __init__(self, input_file: Union[str, Path]): + def __init__(self, input_file: str | Path, **kwargs: dict): """Initialize MicrochipTP4100 object given input_file and determines probe identity from file headers""" - super().__init__(input_file=input_file) + super().__init__(input_file=input_file, **kwargs) self.header = self.get_header() self.probe_key = ProbeKey( ip_address=self.header.get("host"), probe_id=self.header.get("probe_id", None) or "1-1" diff --git a/opensampl/vendors/microchip/twst.py b/opensampl/vendors/microchip/twst.py index bf91f5b..2ead581 100644 --- a/opensampl/vendors/microchip/twst.py +++ b/opensampl/vendors/microchip/twst.py @@ -4,7 +4,7 @@ import re from datetime import timedelta from pathlib import Path -from typing import ClassVar, Optional, Union +from typing import ClassVar import click import numpy as np @@ -34,23 +34,23 @@ class RandomDataConfig(RandomDataMixin.RandomDataConfig): """Model for storing random data generation configurations as provided by CLI or YAML""" # Time series parameters - base_value: Optional[float] = Field( + base_value: float | None = Field( default_factory=lambda: random.uniform(-1e-8, 1e-8), description="random.uniform(-1e-8, 1e-8)" ) - noise_amplitude: Optional[float] = Field( + noise_amplitude: float | None = Field( default_factory=lambda: random.uniform(1e-10, 1e-9), description="random.uniform(1e-10, 1e-9)" ) - drift_rate: Optional[float] = Field( + drift_rate: float | None = Field( default_factory=lambda: random.uniform(-1e-12, 1e-12), description="random.uniform(-1e-12, 1e-12)" ) - ebno_base_value: Optional[float] = Field( + ebno_base_value: float | None = Field( default_factory=lambda: random.uniform(10.0, 20.0), description="random.uniform(10.0, 20.0)" ) - ebno_noise_amplitude: Optional[float] = Field( + ebno_noise_amplitude: float | None = Field( default_factory=lambda: random.uniform(0.5, 2.0), description="random.uniform(0.5, 2.0)" ) - ebno_drift_rate: Optional[float] = Field( + ebno_drift_rate: float | None = Field( default_factory=lambda: random.uniform(-0.01, 0.01), description="random.uniform(-0.01, 0.01)" ) @@ -123,9 +123,9 @@ def get_random_data_cli_options(cls) -> list: ] return vendor_options + base_options - def __init__(self, input_file: Union[str, Path]): + def __init__(self, input_file: str | Path, **kwargs: dict): """Initialize MicrochipTWST object give input_file and determines probe identity from filename""" - super().__init__(input_file=input_file) + super().__init__(input_file=input_file, **kwargs) self.header = self.get_header() self.probe_key = ProbeKey(probe_id="modem", ip_address=self.header["local"]["ip"]) diff --git a/opensampl/vendors/ntp.py b/opensampl/vendors/ntp.py new file mode 100644 index 0000000..c5e634a --- /dev/null +++ b/opensampl/vendors/ntp.py @@ -0,0 +1,773 @@ +"""Probe implementation for NTP vendor""" + +from __future__ import annotations + +import contextlib +import random +import re +import shutil +import socket +import subprocess +import textwrap +import time +from datetime import datetime, timedelta, timezone +from io import StringIO +from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypeVar + +import click +import numpy as np +import pandas as pd +import psycopg2.errors +import requests +import yaml +from loguru import logger +from pydanclick import from_pydantic +from pydantic import BaseModel, ConfigDict, Field +from sqlalchemy.exc import IntegrityError + +from opensampl.load_data import load_probe_metadata +from opensampl.metrics import METRICS, MetricType +from opensampl.mixins.collect import CollectMixin +from opensampl.mixins.random_data import RandomDataMixin +from opensampl.references import REF_TYPES, ReferenceType +from opensampl.vendors.base_probe import BaseProbe +from opensampl.vendors.constants import VENDORS, ProbeKey + +T = TypeVar("T") + +if TYPE_CHECKING: + from collections.abc import Callable + from pathlib import Path + + +def _merge(a: T | None, b: T | None) -> T | None: + return a if a is not None else b + + +class NTPCollector(BaseModel): + """Base class for NTP Collector, for specific implementations to inherit.""" + + mode: ClassVar[Literal["remote", "local"]] + metric_map: ClassVar[dict[str, MetricType]] = { + "phase_offset_s": METRICS.PHASE_OFFSET, + "delay_s": METRICS.DELAY, + "jitter_s": METRICS.JITTER, + "stratum": METRICS.STRATUM, + "reachability": METRICS.REACHABILITY, + "dispersion_s": METRICS.DISPERSION, + "root_delay_s": METRICS.NTP_ROOT_DELAY, + "root_dispersion_s": METRICS.NTP_ROOT_DISPERSION, + "poll_interval_s": METRICS.POLL_INTERVAL, + "sync_health": METRICS.SYNC_HEALTH, + } + + target_host: str + + sync_status: str = Field("unknown") + sync_health: float | None = Field(None, json_schema_extra={"metric": True}) + + stratum: int | None = Field(None, json_schema_extra={"metric": True}) + reachability: int | None = Field(None, json_schema_extra={"metric": True}) + offset_s: float | None = Field(None, serialization_alias="phase_offset_s", json_schema_extra={"metric": True}) + delay_s: float | None = Field(None, json_schema_extra={"metric": True}) + jitter_s: float | None = Field(None, json_schema_extra={"metric": True}) + reference_id: str | None = None + observation_sources: list[str] = Field(default_factory=list) + collection_id: str + collection_ip: str + probe_id: str | None = None + + extras: dict = Field(default_factory=dict, serialization_alias="additional_metadata") + model_config = ConfigDict(serialize_by_alias=True) + + def collect(self): + """Collect a single NTP Reading""" + raise NotImplementedError + + def export_data(self) -> list[CollectMixin.DataArtifact]: + """ + Export the data from the NTP Collection to a list of DataArtifacts + + Each distinct metric type will get it's own data artifact + """ + now = datetime.now(tz=timezone.utc) + include_list = { + f + for f, field_info in type(self).model_fields.items() + if field_info.json_schema_extra and field_info.json_schema_extra.get("metric", False) + } + reference_type, compound_reference = self.determine_reference() + metric_values = self.model_dump(include=include_list, exclude_none=True) + + artifacts: list[CollectMixin.DataArtifact] = [] + for m, v in metric_values.items(): + metric = self.metric_map.get(m, None) + if metric is None: + metric = MetricType( + name=m, + description=f"Automatically generated metric type for {m}", + value_type=object, + unit="unknown", + ) + logger.warning(f"Generated new metric type for {m}") + value = pd.DataFrame([(now, v)], columns=["time", "value"]) + value["time"] = pd.to_datetime(value["time"]) + + artifacts.append( + CollectMixin.DataArtifact( + metric=metric, reference_type=reference_type, compound_reference=compound_reference, value=value + ) + ) + return artifacts + + def export_metadata(self) -> dict[str, Any]: + """Export the metadata from the NTP Collection to a dict""" + include_list = { + f + for f, field_info in type(self).model_fields.items() + if not field_info.json_schema_extra or not field_info.json_schema_extra.get("metric", False) + } + meta = self.model_dump(include=include_list, exclude_none=True) + meta["mode"] = self.mode + return meta + + def export(self) -> CollectMixin.CollectArtifact: + """Export the data + metadata for the NTP Collection to a CollectArtifact""" + meta = self.export_metadata() + + artifacts: list[CollectMixin.DataArtifact] = self.export_data() + + return CollectMixin.CollectArtifact(data=artifacts, metadata=meta) + + @classmethod + def invert_metric_map(cls) -> dict[str, str]: + """Invert metric map to go from MetricType.name to string""" + return {v.name: k for k, v in cls.metric_map.items()} + + def determine_reference(self) -> tuple[ReferenceType, None | dict[str, Any]]: + """Get the reference type and compound reference details""" + return REF_TYPES.PROBE, {"ip_address": self.collection_ip, "probe_id": self.collection_id} + + +class NTPLocalCollector(NTPCollector): + """Collector model for taking NTP readings from local device""" + + mode: ClassVar[Literal["remote", "local"]] = "local" + + @staticmethod + def _run(cmd: list[str], timeout: float = 8.0) -> str | None: + """Run command; return stdout or None if missing/failed.""" + bin0 = cmd[0] + if shutil.which(bin0) is None: + logger.debug(f"ntp local: command {bin0!r} not found") + return None + try: + proc = subprocess.run( # noqa: S603 + cmd, + capture_output=True, + text=True, + timeout=timeout, + check=False, + ) + except (OSError, subprocess.SubprocessError) as e: + logger.debug(f"ntp local: command {cmd!r} failed: {e}") + return None + if proc.returncode != 0: + logger.debug(f"ntp local: {cmd!r} exit {proc.returncode}: {proc.stderr!r}") + return None + logger.debug(f"ntp local: {cmd!r} exit {proc.stdout}") + return proc.stdout or "" + + def _parse_chronyc_tracking(self, text: str) -> None: + """Parse `chronyc tracking` key: value output.""" + out: dict[str, Any] = {} + for l in text.splitlines(): + line = l.strip() + if not line or ":" not in line: + continue + key, _, rest = line.partition(":") + key = key.strip().lower().replace(" ", "_") + val = rest.strip() + out[key] = val + + # Last offset : +0.000000123 seconds + m = re.search(r"last offset\s*:\s*([+-]?[\d.eE+-]+)\s*seconds?", text, re.IGNORECASE) + if m: + with contextlib.suppress(ValueError): + self.offset_s = _merge(self.offset_s, float(m.group(1))) + + m = re.search(r"rms offset\s*:\s*([+-]?[\d.eE+-]+)\s*seconds?", text, re.IGNORECASE) + if m: + with contextlib.suppress(ValueError): + self.jitter_s = _merge(self.jitter_s, float(m.group(1))) + + m = re.search(r"stratum\s*:\s*(\d+)", text, re.IGNORECASE) + if m: + with contextlib.suppress(ValueError): + self.stratum = _merge(self.stratum, int(m.group(1))) + + m = re.search(r"reference id\s*:\s*(\S+)(?:\s*\(([^)]+)\))?", text, re.IGNORECASE) + if m: + self.reference_id = (m.group(2) or m.group(1)) or self.reference_id + + self.sync_status = "unsynchronized" + if "normal" in text.lower() or self.offset_s is not None: + self.sync_status = "tracking" + self.extras["chronyc_raw_tracking"] = out + self.observation_sources.append("chronyc_tracking") + + def _parse_chronyc_sources(self, text: str) -> None: + """Parse `chronyc sources` for reach and selected source.""" + reach: int | None = None + selected: str | None = None + for l in text.splitlines(): + line = l.strip() + if not line or line.startswith(("MS", "=")): + continue + # ^* or ^+ prefix indicates selected/accepted + if line.startswith(("*", "+")): + parts = line.split() + if len(parts) >= 7: + try: + reach = int(parts[5], 8) if parts[5].startswith("0") else int(parts[5]) + except ValueError: + with contextlib.suppress(ValueError): + reach = int(parts[5]) + selected = parts[1] + break + # Fallback: last column often reach (octal) + parts = line.split() + if len(parts) >= 7 and parts[0] in ("^*", "^+", "*", "+"): + # already handled + pass + if reach is None: + # Try any line with 377 octal style + m = re.search(r"\b([0-7]{3})\b", text) + if m: + with contextlib.suppress(ValueError): + reach = int(m.group(1), 8) + + self.reachability = self.reachability or reach + self.reference_id = self.reference_id or selected + self.observation_sources.append("chronyc_sources") + + def _parse_ntpq(self, text: str) -> None: + """Parse `ntpq -p` / `ntpq -pn` output.""" + offset_s: float | None = None + delay_s: float | None = None + jitter_s: float | None = None + stratum: int | None = None + reach: int | None = None + ref = None + for l in text.splitlines(): + line = l.strip() + if not line or line.startswith(("remote", "=")): + continue + if line.startswith(("*", "+", "-")): + parts = line.split() + # remote refid st t when poll reach delay offset jitter + if len(parts) >= 10: + with contextlib.suppress(ValueError): + stratum = int(parts[2]) + + try: + delay_s = float(parts[7]) / 1000.0 # ms -> s + offset_s = float(parts[8]) / 1000.0 + jitter_s = float(parts[9]) / 1000.0 + except (ValueError, IndexError): + pass + try: + reach = int(parts[6], 8) if parts[6].startswith("0") else int(parts[6]) + except ValueError: + with contextlib.suppress(ValueError): + reach = int(parts[6]) + + ref = parts[1] + break + sync_status = "synced" if offset_s is not None else "unknown" + + self.offset_s = self.offset_s or offset_s + self.delay_s = self.delay_s or delay_s + self.jitter_s = self.jitter_s or jitter_s + self.stratum = self.stratum or stratum + self.reachability = self.reachability or reach + self.reference_id = self.reference_id or ref + self.sync_status = sync_status or self.sync_status + self.observation_sources.append("ntpq") + + def _parse_timedatectl(self, text: str) -> None: + """Parse `timedatectl status` / `show-timesync --all`.""" + sync = None + for line in text.splitlines(): + low = line.lower() + if "system clock synchronized" in low or "ntp synchronized" in low: + if "yes" in low: + sync = True + elif "no" in low: + sync = False + sync_status = "unknown" + if sync is True: + sync_status = "synchronized" + elif sync is False: + sync_status = "unsynchronized" + + if self.sync_status == "unknown": + self.sync_status = sync_status or self.sync_status + self.observation_sources.append("timedatectl") + self.extras["timedatectl"] = text[:2000] + + def _parse_systemctl_show(self, text: str) -> None: + """Parse `systemctl show` / `systemctl status` for systemd-timesyncd.""" + active = None + for line in text.splitlines(): + if line.strip().lower().startswith("activestate="): + active = line.split("=", 1)[1].strip().lower() == "active" + break + if active is None and "active (running)" in text.lower(): + active = True + sync_status = "unknown" + if active is True: + sync_status = "service_active" + elif active is False: + sync_status = "service_inactive" + + if self.sync_status == "unknown": + self.sync_status = sync_status or self.sync_status + self.extras["systemctl"] = text[:2000] + self.observation_sources.append("systemctl_timesyncd") + + def collect(self): + """Collect local NTP readings using various tools""" + t = self._run(["chronyc", "tracking"]) + if t: + self._parse_chronyc_tracking(t) + + t = self._run(["chronyc", "sources", "-v"]) or self._run(["chronyc", "sources"]) + if t: + self._parse_chronyc_sources(t) + + if self.offset_s is None and self.stratum is None: + t = self._run(["ntpq", "-pn"]) or self._run(["ntpq", "-p"]) + if t: + self._parse_ntpq(t) + + t = self._run(["timedatectl", "show-timesync", "--all"]) or self._run(["timedatectl", "status"]) + if t: + self._parse_timedatectl(t) + + t = self._run(["systemctl", "show", "systemd-timesyncd", "--property=ActiveState"]) + if not t: + t = self._run(["systemctl", "status", "systemd-timesyncd", "--no-pager"]) + + if t: + self._parse_systemctl_show(t) + + if not self.observation_sources: + self.observation_sources = ["none"] + + self.sync_health = 1.0 if self.sync_status in ("tracking", "synchronized", "synced") else 0.0 + + if self.probe_id is None: + self.probe_id = "ntp-local" + + +class NTPRemoteCollector(NTPCollector): + """Collector model for taking readings from remote NTP Server.""" + + mode: ClassVar[Literal["remote", "local"]] = "remote" + + target_port: int + timeout: float = 3.0 + + root_delay_s: float | None = Field(None, json_schema_extra={"metric": True}) + root_dispersion_s: float | None = Field(None, json_schema_extra={"metric": True}) + poll_interval_s: float | None = Field(None, json_schema_extra={"metric": True}) + leap_status: str = "unknown" + + def configure_failure(self, e: Exception) -> None: + """Set all metric and metadata values to reflect failure to connect""" + self.sync_status = "unreachable" + self.sync_health = 0 + self.extras["error"] = str(e) + self.observation_sources.append("ntplib") + self.observation_sources.append("error") + + def _estimate_jitter_s(self) -> None: + """ + Single NTP client response does not include RFC5905 peer jitter (that needs multiple samples). + + Emit a conservative positive bound from round-trip delay and root dispersion so downstream + ``NTP Jitter`` metrics and dashboards have a value; chrony/ntpq local paths still supply + true jitter when available. + """ + if self.delay_s is None and self.root_dispersion_s is None: + return + d = float(self.delay_s) if self.delay_s is not None else 0.0 + r = float(self.root_dispersion_s) if self.root_dispersion_s is not None else 0.0 + est = 0.05 * d + 0.25 * r + if est > 0: + self.jitter_s = est + return + + def collect(self): + """Collect readings from a single ping against a remote NTP server.""" + try: + import ntplib # type: ignore[import-untyped] + except ImportError as e: + raise ImportError( + "Remote NTP collection requires the 'ntplib' package (install opensampl[collect])." + ) from e + client = ntplib.NTPClient() + try: + resp = client.request(self.target_host, port=self.target_port, version=3, timeout=self.timeout) + except Exception as e: + logger.warning(f"NTP request to {self.target_host}:{self.target_port} failed: {e}") + self.configure_failure(e) + return + leap = int(resp.leap) + leap_map = {0: "no_warning", 1: "add_second", 2: "del_second", 3: "alarm"} + self.leap_status = leap_map.get(leap, str(leap)) + + stratum = int(resp.stratum) + self.stratum = stratum + + try: + self.poll_interval_s = float(2 ** int(resp.poll)) + except (TypeError, ValueError, OverflowError): + logger.debug("No poll interval determined") + + self.root_delay_s = float(resp.root_delay) if resp.root_delay is not None else None + self.root_dispersion_s = float(resp.root_dispersion) if resp.root_dispersion is not None else None + self.delay_s = float(resp.delay) if resp.delay is not None else None + self.offset_s = float(resp.offset) if resp.offset is not None else None + + ref_id = getattr(resp, "ref_id", None) + if hasattr(ref_id, "decode"): + try: + ref_id = ref_id.decode("ascii", errors="replace") + except Exception: + ref_id = str(ref_id) + self.reference_id = str(ref_id) if ref_id is not None else None + + sync_ok = stratum < 16 and self.offset_s is not None + self.observation_sources.append("ntplib") + self.sync_status = "synchronized" if sync_ok else "unsynchronized" + self.sync_health = 1.0 if sync_ok else 0.0 + self._estimate_jitter_s() + + self.extras["version"] = getattr(resp, "version", None) + + if self.probe_id is None: + self.probe_id = f"remote:{self.target_port}" + + +def collect_ip_factory() -> str: + """Get ip address for collection host using socket (default to 127.0.0.1)""" + s = None + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) # doesn't actually send data + v = s.getsockname()[0] + except Exception: + v = "127.0.0.1" + finally: + if s: + s.close() + return v + + +def collect_id_factory() -> str: + """Get humanreadable host name for collection host using socket (default to collection-host)""" + try: + return socket.gethostname() or "collection-host" + except Exception: + return "collection-host" + + +class NtpProbe(BaseProbe, CollectMixin, RandomDataMixin): + """Probe parser for NTP vendor data files""" + + vendor = VENDORS.NTP + + class CollectConfig(CollectMixin.CollectConfig): + """ + Configuration for Collecting NTP Readings + + Attributes: + probe_id: stable probe_id slug (e.g. local-chrony) + ip_address: Host or IP address for Probe (default '127.0.0.1') + port: UDP port for remote mode (use high ports for lab mocks) + output_dir: When provided, will save collected data as a file to provided directory. Filename will be + automatically generated as NTP_{ip_address}_{probe_id}_{vendor}_{timestamp}.txt + load: Whether to load collected data directly to the database + duration: Number of seconds to collect data for + mode: Collect remote or local NTP. Default is 'local'. + interval: Seconds between samples; 0 = single sample and exit + duration: Samples to collect when interval > 0 + timeout: UDP request timeout for remote mode(seconds) default: 3.0 + collection_ip: Override for the IP address of device collecting readings. Will attempt to resolve a local + network IP using socket and fall back to '127.0.0.1' + collection_id: Override for the Probe ID of the device collecting readings. Will attempt to resolve using + socket.gethostname and fall back to 'collection-host' + + """ + + ip_address: str = "127.0.0.1" + port: int = 123 + mode: Literal["remote", "local"] = "local" + interval: float = Field(0.0, ge=0.0) + duration: int = Field(1, ge=1) + timeout: float = 3.0 + collection_ip: str = Field(default_factory=collect_ip_factory) + collection_id: str = Field(default_factory=collect_id_factory) + + @classmethod + def get_collect_cli_options(cls) -> list[Callable]: + """Get the decorators to generate collection options for CLI""" + return [ + from_pydantic(cls.CollectConfig, rename={"ip_address": "host", "duration": "count"}), + click.pass_context, + ] + + class RandomDataConfig(RandomDataMixin.RandomDataConfig): + """Random NTP-like test data.""" + + base_value: float = Field( + default_factory=lambda: random.uniform(-1e-4, 1e-4), + description="random.uniform(-1e-4, 1e-4)", + ) + noise_amplitude: float = Field( + default_factory=lambda: random.uniform(1e-9, 1e-7), + description="random.uniform(1e-9, 1e-7)", + ) + drift_rate: float = Field( + default_factory=lambda: random.uniform(-1e-12, 1e-12), + description="random.uniform(-1e-12, 1e-12)", + ) + + def __init__(self, input_file: str | Path, **kwargs: dict): + """Initialize NtpProbe from input file""" + super().__init__(input_file=input_file, **kwargs) + self.collection_probe = None + + def process_metadata(self) -> dict: + """ + Parse and return probe metadata from input file. + + Returns: + dict with metadata field names as keys + + """ + if not self.metadata_parsed: + header_lines = [] + with self.input_file.open() as f: + for line in f: + if line.startswith("#"): + header_lines.append(line[2:]) + else: + break + + header_str = "".join(header_lines) + self.metadata = yaml.safe_load(header_str) + self.collection_probe = ProbeKey( + ip_address=self.metadata.get("collection_ip"), probe_id=self.metadata.get("collection_id") + ) + load_probe_metadata(vendor=self.vendor, probe_key=self.collection_probe, data={"reference": True}) + self.probe_key = ProbeKey( + ip_address=self.metadata.get("target_host"), probe_id=self.metadata.get("probe_id") + ) + self.metadata_parsed = True + + return self.metadata + + @classmethod + def load_metadata(cls, probe_key: ProbeKey, metadata: dict) -> None: + """ + Parse and return probe metadata from input file. + + Returns: + dict with metadata field names as keys + + """ + collection_probe = ProbeKey(ip_address=metadata.get("collection_ip"), probe_id=metadata.get("collection_id")) + load_probe_metadata(vendor=cls.vendor, probe_key=collection_probe, data={"reference": True}) + load_probe_metadata(vendor=cls.vendor, probe_key=probe_key, data=metadata) + + def process_time_data(self) -> None: + """ + Parse and load time series data from self.input_file. + + Use either send_time_data (which prefills METRICS.PHASE_OFFSET) + or send_data and provide alternative METRICS type. + Both require a df as follows: + pd.DataFrame with columns: + - time (datetime64[ns]): timestamp for each measurement + - value (float64): measured value at each timestamp + + """ + raw_df = pd.read_csv( + self.input_file, + comment="#", + ) + self.process_metadata() + + reference_type = REF_TYPES.PROBE + grouped_dfs: dict[str, pd.DataFrame] = { + str(metric): group.reset_index(drop=True) for metric, group in raw_df.groupby("metric") + } + for metr, df in grouped_dfs.items(): + metric = NTPCollector.metric_map.get(metr) + if not metric: + logger.warning(f"Metric {metr} is not supported for NTP. Will not ingest {len(df)} rows") + continue + try: + self.send_data( + data=df, + metric=metric, + reference_type=reference_type, + compound_reference=self.collection_probe.model_dump(), + ) + except requests.HTTPError as e: + resp = e.response + if resp is None: + raise + status_code = resp.status_code + if status_code == 409: + logger.info(f"{metr} against {self.collection_probe} already loaded for time frame, continuing..") + continue + raise + except IntegrityError as e: + if isinstance(e.orig, psycopg2.errors.UniqueViolation): # ty: ignore[unresolved-attribute] + logger.info( + f"{metr} against {self.collection_probe} already loaded for time " + f"frame already loaded for time frame, continuing.." + ) + + @classmethod + def collect(cls, collect_config: CollectConfig) -> CollectMixin.CollectArtifact: + """Collect readings for an NTP probe according to collect_config.""" + collector_overrides = collect_config.model_dump( + include=["collection_ip", "collection_id", "probe_id"], exclude_none=True + ) + + def collect_once() -> CollectMixin.CollectArtifact: + collector = None + if collect_config.mode == "local": + collector = NTPLocalCollector(target_host=collect_config.ip_address, **collector_overrides) + elif collect_config.mode == "remote": + collector = NTPRemoteCollector( + target_host=collect_config.ip_address, + target_port=collect_config.port, + timeout=collect_config.timeout, + **collector_overrides, + ) + if collector is None: + raise ValueError("Could not determine mode from collect_config") + collector.collect() + + return collector.export() + + if collect_config.interval <= 0: + return collect_once() + + artifact = None + sample_count = max(collect_config.duration, 1) + for sample_idx in range(sample_count): + newer = collect_once() + if artifact is None: + artifact = newer + else: + artifact.data.extend(newer.data) + artifact.metadata |= newer.metadata + + if sample_idx < sample_count - 1: + time.sleep(collect_config.interval) + + return artifact + + @classmethod + def create_file_content(cls, collected: CollectMixin.CollectArtifact) -> str: + """Create the content of a file from the CollectArtifacts""" + metric_names = NTPCollector.invert_metric_map() + dfs = [] + for d in collected.data or []: + df = d.value + df["metric"] = metric_names.get(d.metric.name, d.metric.name.lower().replace(" ", "_")) + dfs.append(df) + value_df = pd.concat(dfs) if dfs else None + + header = yaml.dump(collected.metadata, sort_keys=False) + header = textwrap.indent(header, prefix="# ") + buffer = StringIO() + buffer.write(header) + buffer.write("\n") + + if value_df is not None: + # write dataframe + value_df.to_csv(buffer, index=False) + + return buffer.getvalue() + + @classmethod + def generate_random_data( + cls, + config: RandomDataConfig, + probe_key: ProbeKey, + ) -> ProbeKey: + """Generate synthetic NTP-like metrics for testing.""" + cls._setup_random_seed(config.seed) + logger.info(f"Generating random NTP data for {probe_key}") + + meta = { + "mode": "random", + "name": f"Random NTP {probe_key}", + "target_host": "", + "target_port": 0, + "sync_status": "tracking", + "leap_status": "no_warning", + "observation_sources": ["random"], + "additional_metadata": {"test_data": True}, + } + cls._send_metadata_to_db(probe_key, meta) + + total_seconds = config.duration_hours * 3600 + num_samples = int(total_seconds / config.sample_interval) + times = [] + metric_maps = { + "offset": {"metric": METRICS.PHASE_OFFSET, "values": []}, + "delay_s": {"metric": METRICS.DELAY, "values": []}, + "jitter_s": {"metric": METRICS.JITTER, "values": []}, + "stratum": {"metric": METRICS.STRATUM, "values": []}, + "sync_health": {"metric": METRICS.SYNC_HEALTH, "values": []}, + } + + for i in range(num_samples): + sample_time = config.start_time + timedelta(seconds=i * config.sample_interval) + times.append(sample_time) + time_offset = i * config.sample_interval + drift_component = config.drift_rate * time_offset + noise = float(np.random.normal(0, config.noise_amplitude)) + offset = config.base_value + drift_component + noise + if random.random() < config.outlier_probability: + offset += float(np.random.normal(0, config.noise_amplitude * config.outlier_multiplier)) + + delay_s = 0.02 + abs(0.0001 * random.random()) + jitter_s = abs(float(config.noise_amplitude * 5)) + stratum = 2 + (1 if random.random() < 0.05 else 0) + sync_health = 1.0 + metric_maps["offset"]["values"].append(offset) + metric_maps["delay_s"]["values"].append(delay_s) + metric_maps["jitter_s"]["values"].append(jitter_s) + metric_maps["stratum"]["values"].append(stratum) + metric_maps["sync_health"]["values"].append(sync_health) + + for metric in metric_maps.values(): + cls.send_data( + probe_key=probe_key, + metric=metric.get("metric"), + reference_type=REF_TYPES.UNKNOWN, + data=pd.DataFrame({"time": times, "value": metric.get("values")}), + ) + + logger.info(f"Finished random NTP generation for {probe_key}") + return probe_key diff --git a/pyproject.toml b/pyproject.toml index d297b5d..acfd712 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,10 +18,11 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Natural Language :: English", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Operating System :: OS Independent", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Physics", @@ -37,7 +38,7 @@ classifiers = [ "Framework :: Pydantic", "Framework :: Pydantic :: 2" ] -requires-python = ">=3.9,<3.13" +requires-python = ">=3.10,<3.15" dependencies = [ "pydantic>=2.10.3,<3", "pydantic-settings>=2.9.0", @@ -54,7 +55,7 @@ dependencies = [ "loguru>=0.7.0,<0.8", "psycopg2-binary>=2.9.0,<3", "python-dotenv", - "python-multipart>=0.0.20,<0.0.21", + "python-multipart>=0.0.26,<0.0.27", "astor", "libcst", "jinja2>=3.1.6", @@ -77,7 +78,10 @@ backend = [ "uvicorn", "prometheus-client", ] -collect = ["telnetlib3==2.0.4"] +collect = [ + "telnetlib3==2.0.4", + "ntplib>=0.4.0,<0.5" +] [project.scripts] opensampl = "opensampl.cli:cli" @@ -97,6 +101,8 @@ dev = [ "mkdocs-gen-files", "mkdocs-material", "mkdocs-click", + "psycopg[binary]", + "pytest-postgresql" ] [tool.hatch.build.targets.sdist] @@ -131,7 +137,7 @@ build-backend = "hatchling.build" [tool.ruff] line-length = 120 -exclude = [".git", "__pycache__", "venv", "env", ".venv", ".env", "build", "dist", "docs", "opensampl/server/migrations/**/*.py",] +exclude = [".git", "__pycache__", "venv", "env", ".venv", ".env", "build", "dist", "docs", "opensampl/server/migrations/**/*.py"] include = ["opensampl/**/*.py"] [tool.ruff.lint] @@ -141,12 +147,13 @@ select = ["F", "E", "W", "C", "I", "D", "N", "B", "ERA", "ANN", "S", "A", "COM", "FLY", "PERF", "PL", "UP", "FURB", "RUF", "TRY"] ignore = ["D203", "D212", "D400", "D415", "ANN401", "S101", "PLR2004", "COM812", "ANN201", "B011", "EM102", "TRY003", "ANN204", "FA100", "PIE790", "EM101", - "PLC0415"] + "PLC0415", 'E741'] [tool.ruff.lint.per-file-ignores] "opensampl/vendors/**/*.py" = ['S311'] # we want to ignore the errors about random "opensampl/server/backend/main.py" = ['B008', 'ARG001'] #ignore complaints about calling functions in args "opensampl/mixins/random_data.py" = ['S311'] + [tool.ruff.lint.pylint] max-args = 10 diff --git a/scripts/gen_api_docs.py b/scripts/gen_api_docs.py index daf6373..d792658 100644 --- a/scripts/gen_api_docs.py +++ b/scripts/gen_api_docs.py @@ -94,7 +94,7 @@ def generate_api_docs_and_nav(): nav_structure = [{"index": f"{API_DIR_NAME}/index.md"}] for py_file in sorted(SRC_DIR.rglob("*.py")): - if py_file.stem == "__init__": + if py_file.stem == "__init__" or 'migration' in str(py_file): continue rel_path = py_file.relative_to(SRC_DIR) doc_path = DOCS_API_DIR / rel_path.with_suffix(".md") diff --git a/tests/conftest.py b/tests/conftest.py index 0c8b417..44f756e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -60,6 +60,7 @@ def mock_config(): config.ROUTE_TO_BACKEND = False config.DATABASE_URL = "sqlite:///:memory:" config.LOG_LEVEL = "DEBUG" + config.ENABLE_GEOLOCATE = False mock.return_value = config yield mock diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 0000000..1ccf890 --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,332 @@ +""" +tests/conftest.py + +Shared pytest fixtures for openSAMPL integration tests. + +Prerequisites +------------- +- PostgreSQL with PostGIS extension available +- pytest-postgresql installed: + uv add --group dev pytest-postgresql +- On macOS (Homebrew): + brew install postgresql postgis + +pytest-postgresql will locate pg_ctl automatically from your PATH. If you +have multiple Postgres versions installed, point it at the right one via +pytest.ini or pyproject.toml: + + [tool.pytest.ini_options] + postgresql_exec = "/opt/homebrew/opt/postgresql@16/bin/pg_ctl" +""" + +import pytest +from pytest_postgresql import factories as pg_factories +from sqlalchemy import create_engine, text +from sqlalchemy.orm import Session, sessionmaker + +from opensampl.db.orm import Base +from opensampl.db.orm import Defaults as DBDefaults +from opensampl.db.orm import MetricType as DBMetricType +from opensampl.db.orm import Reference as DBReference +from opensampl.db.orm import ReferenceType as DBReferenceType +from opensampl.metrics import METRICS, MetricType +from opensampl.references import REF_TYPES, ReferenceType + + +# --------------------------------------------------------------------------- +# pytest-postgresql process fixture +# +# postgresql_proc manages the Postgres server lifetime (session-scoped). +# We deliberately avoid the postgresql connection fixture so we have no +# dependency on a specific psycopg version — the project already has +# psycopg2-binary, and SQLAlchemy handles the connection from here. +# --------------------------------------------------------------------------- + +postgresql_proc = pg_factories.postgresql_proc() + + +# --------------------------------------------------------------------------- +# Helpers: introspect METRICS / REF_TYPES the same way VENDORS.all() does +# --------------------------------------------------------------------------- + +def _all_metrics() -> list[MetricType]: + """All MetricType instances defined on the METRICS class.""" + return [v for v in METRICS.__dict__.values() if isinstance(v, MetricType)] + + +def _all_ref_types() -> list[ReferenceType]: + """All ReferenceType instances defined on REF_TYPES (includes CompoundReferenceType).""" + return [v for v in REF_TYPES.__dict__.values() if isinstance(v, ReferenceType)] + + +# --------------------------------------------------------------------------- +# Session-scoped engine +# Schema, tables, seed data, and the get_default_uuid_for stub are all +# created once per test session. +# --------------------------------------------------------------------------- + +@pytest.fixture(scope="session") +def db_engine(postgresql_proc): + """ + Session-scoped SQLAlchemy engine pointed at the pytest-postgresql instance. + + Connects using psycopg2-binary (already a project dependency) so there is + no dependency on psycopg3. Creates the opensampl_test database on first + run via a temporary autocommit connection to the default 'postgres' database. + + Lifecycle: + 1. Create the opensampl_test database. + 2. Install PostGIS and create the castdb schema. + 3. Create all ORM tables via Base.metadata.create_all(). + 4. Seed metric_type, reference_type, reference, and defaults tables. + 5. Install the get_default_uuid_for() PL/pgSQL stub. + """ + # postgresql_proc exposes plain attributes — no psycopg version dependency + host = postgresql_proc.host + port = postgresql_proc.port + user = postgresql_proc.user + test_dbname = "opensampl_test" + + # Connect to the default 'postgres' db to create our test database. + # Must use isolation_level=AUTOCOMMIT because CREATE DATABASE cannot run + # inside a transaction block. + bootstrap_url = f"postgresql+psycopg2://{user}@{host}:{port}/postgres" + bootstrap_engine = create_engine(bootstrap_url, isolation_level="AUTOCOMMIT") + with bootstrap_engine.connect() as conn: + exists = conn.execute( + text("SELECT 1 FROM pg_database WHERE datname = :dbname"), + {"dbname": test_dbname}, + ).fetchone() + if not exists: + conn.execute(text(f'CREATE DATABASE "{test_dbname}"')) + bootstrap_engine.dispose() + + db_url = f"postgresql+psycopg2://{user}@{host}:{port}/{test_dbname}" + engine = create_engine(db_url, echo=False) + + with engine.begin() as conn: + # PostGIS is required by the Locations.geom column (GeoAlchemy2) + conn.execute(text("CREATE EXTENSION IF NOT EXISTS postgis")) + conn.execute(text(f"CREATE SCHEMA IF NOT EXISTS {Base.metadata.schema}")) + + Base.metadata.create_all(engine) + _seed_lookup_tables(engine) + _install_default_uuid_stub(engine) + + yield engine + + engine.dispose() + + +# --------------------------------------------------------------------------- +# Seeding helpers (called once from db_engine, not exposed as fixtures) +# --------------------------------------------------------------------------- + +def _seed_lookup_tables(engine) -> None: + """ + Populate metric_type, reference_type, reference, and defaults tables. + + Reads directly from the METRICS and REF_TYPES Python definitions so the + test DB always matches what the application expects — no hardcoded values. + After inserting the lookup rows, seeds the defaults table with the UUIDs + of the UNKNOWN rows, mirroring how production initialises get_default_uuid_for(). + """ + SessionLocal = sessionmaker(bind=engine) + session = SessionLocal() + + try: + # --- metric_type --- + for metric in _all_metrics(): + data = metric.model_dump() # value_type serialised to str by field_serializer + if not session.query(DBMetricType).filter_by(name=data["name"]).first(): + session.add(DBMetricType(**data)) + + # --- reference_type --- + # CompoundReferenceType.model_dump() includes reference_table; the column is nullable so plain + # ReferenceType rows (no reference_table) are stored with NULL, which is correct. + for ref_type in _all_ref_types(): + data = ref_type.model_dump() + if not session.query(DBReferenceType).filter_by(name=data["name"]).first(): + session.add(DBReferenceType(**data)) + + session.flush() + + # --- reference: one default UNKNOWN row -------------------------- + # get_default_uuid_for('reference') needs at least one reference row to + # point at. We use the UNKNOWN reference type with no compound target. + unknown_ref_type = session.query(DBReferenceType).filter_by(name=REF_TYPES.UNKNOWN.name).one() + default_reference = session.query(DBReference).filter_by( + reference_type_uuid=unknown_ref_type.uuid, + compound_reference_uuid=None, + ).first() + if not default_reference: + default_reference = DBReference( + reference_type_uuid=unknown_ref_type.uuid, + compound_reference_uuid=None, + ) + session.add(default_reference) + + session.flush() + + # --- defaults table --------------------------------------------- + # Maps table/category names to the UUID that get_default_uuid_for() + # should return. Mirrors what the production TimescaleDB init does. + unknown_metric = session.query(DBMetricType).filter_by(name=METRICS.UNKNOWN.name).one() + + for table_name, uuid_value in [ + ("metric_type", unknown_metric.uuid), + ("reference", default_reference.uuid), + ]: + if not session.query(DBDefaults).filter_by(table_name=table_name).first(): + session.add(DBDefaults(table_name=table_name, uuid=uuid_value)) + + session.commit() + + except Exception: + session.rollback() + raise + finally: + session.close() + + +def _install_default_uuid_stub(engine) -> None: + """ + Install the get_default_uuid_for() PL/pgSQL stub. + + Rather than hardcoding UUIDs, the stub queries the defaults table — the + same approach the production TimescaleDB function uses. This means it + automatically returns whatever was seeded above. + """ + schema = Base.metadata.schema + + with engine.begin() as conn: + conn.execute(text(f""" + CREATE OR REPLACE FUNCTION get_default_uuid_for(entity_type TEXT) + RETURNS TEXT AS $$ + DECLARE + result_uuid TEXT; + BEGIN + SELECT uuid + INTO result_uuid + FROM {schema}.defaults + WHERE table_name = entity_type; + + RETURN result_uuid; + END; + $$ LANGUAGE plpgsql; + """)) + + +# --------------------------------------------------------------------------- +# Per-test session — savepoint rollback keeps tests isolated +# --------------------------------------------------------------------------- + +@pytest.fixture +def db_session(db_engine) -> Session: + """ + Function-scoped database session backed by a savepoint. + + Every test starts with the full seeded dataset intact. Any rows inserted + or updated during the test are rolled back when the test ends without + disturbing the seeded rows, and without the cost of recreating the schema. + + Usage:: + + def test_something(db_session): + factory = TableFactory("locations", db_session) + factory.write({"name": "test-loc", "lat": 35.9, "lon": -84.3}) + result = db_session.query(Locations).filter_by(name="test-loc").one() + assert result.name == "test-loc" + # row is gone after the test + """ + connection = db_engine.connect() + outer_transaction = connection.begin() + session = Session(bind=connection) + session.begin_nested() # SAVEPOINT — inner rollback target + + yield session + + session.close() + outer_transaction.rollback() # wipes everything written during the test + connection.close() + + +# --------------------------------------------------------------------------- +# Seeded UUIDs — expose the canonical lookup UUIDs tests may need directly +# --------------------------------------------------------------------------- + +@pytest.fixture(scope="session") +def seeded_uuids(db_engine) -> dict: + """ + Session-scoped dict of UUIDs inserted during seed, keyed by logical name. + + Useful when a test needs to construct ORM objects by hand (e.g. ProbeData) + and must reference real FK values. + + Keys + ---- + metric_type. — UUID from the metric_type table + reference_type. — UUID from the reference_type table + reference.unknown — UUID of the default UNKNOWN reference row + default.metric_type — what get_default_uuid_for('metric_type') returns + default.reference — what get_default_uuid_for('reference') returns + + Example:: + + def test_probe_data(db_session, seeded_uuids): + phase_offset_uuid = seeded_uuids["metric_type.Phase Offset"] + """ + SessionLocal = sessionmaker(bind=db_engine) + session = SessionLocal() + + try: + uuids: dict = {} + + for row in session.query(DBMetricType).all(): + uuids[f"metric_type.{row.name}"] = row.uuid + + for row in session.query(DBReferenceType).all(): + uuids[f"reference_type.{row.name}"] = row.uuid + + unknown_ref_type = session.query(DBReferenceType).filter_by(name=REF_TYPES.UNKNOWN.name).one() + unknown_ref = session.query(DBReference).filter_by( + reference_type_uuid=unknown_ref_type.uuid, + compound_reference_uuid=None, + ).one() + uuids["reference.unknown"] = unknown_ref.uuid + + for row in session.query(DBDefaults).all(): + uuids[f"default.{row.table_name}"] = row.uuid + + return uuids + + finally: + session.close() + + +# --------------------------------------------------------------------------- +# Routing environment — patch BaseConfig for @route-decorated functions +# --------------------------------------------------------------------------- + +@pytest.fixture +def db_env(db_engine, monkeypatch) -> None: + """ + Set env vars so that BaseConfig routes directly to the test DB. + + Any test that calls a @route-decorated function (write_to_table, + load_time_data, load_probe_metadata, create_new_tables) must include + this fixture. Pass session=db_session explicitly so the route wrapper + uses your test session rather than opening a new one. + + Usage:: + + def test_write_to_table(db_env, db_session): + write_to_table( + "locations", + {"name": "test-loc", "lat": 35.9, "lon": -84.3}, + session=db_session, + ) + """ + monkeypatch.setenv("ROUTE_TO_BACKEND", "false") + monkeypatch.setenv("DATABASE_URL", str(db_engine.url)) + monkeypatch.delenv("BACKEND_URL", raising=False) \ No newline at end of file diff --git a/tests/integration/test_db_setup.py b/tests/integration/test_db_setup.py new file mode 100644 index 0000000..6f1f542 --- /dev/null +++ b/tests/integration/test_db_setup.py @@ -0,0 +1,106 @@ +""" +tests/integration/test_db_setup.py + +Smoke tests to verify the test database spun up and seeded correctly. +""" + +import pytest +from sqlalchemy import text + +from opensampl.db.orm import Defaults as DBDefaults +from opensampl.db.orm import MetricType as DBMetricType +from opensampl.db.orm import Reference as DBReference +from opensampl.db.orm import ReferenceType as DBReferenceType +from opensampl.metrics import METRICS, MetricType +from opensampl.references import REF_TYPES, ReferenceType + + +def test_schema_exists(db_session): + """castdb schema was created.""" + result = db_session.execute( + text("SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'castdb'") + ).scalar() + assert result == "castdb" + + +def test_postgis_installed(db_session): + """PostGIS extension is available (required for Locations.geom).""" + result = db_session.execute(text("SELECT extname FROM pg_extension WHERE extname = 'postgis'")).scalar() + assert result == "postgis" + + +def test_all_metrics_seeded(db_session): + """Every MetricType defined on METRICS is present in the metric_type table.""" + expected = {v.name for v in METRICS.__dict__.values() if isinstance(v, MetricType)} + seeded = {row.name for row in db_session.query(DBMetricType).all()} + assert expected == seeded + + +def test_all_reference_types_seeded(db_session): + """Every ReferenceType defined on REF_TYPES is present in the reference_type table.""" + expected = {v.name for v in REF_TYPES.__dict__.values() if isinstance(v, ReferenceType)} + seeded = {row.name for row in db_session.query(DBReferenceType).all()} + assert expected == seeded + + +def test_default_reference_row_exists(db_session): + """A default UNKNOWN reference row exists for get_default_uuid_for('reference').""" + unknown_ref_type = db_session.query(DBReferenceType).filter_by(name=REF_TYPES.UNKNOWN.name).one() + ref = db_session.query(DBReference).filter_by( + reference_type_uuid=unknown_ref_type.uuid, + compound_reference_uuid=None, + ).first() + assert ref is not None + + +def test_defaults_table_seeded(db_session): + """defaults table has entries for both metric_type and reference.""" + rows = {row.table_name for row in db_session.query(DBDefaults).all()} + assert "metric_type" in rows + assert "reference" in rows + + +def test_get_default_uuid_for_metric_type(db_session): + """Stub function returns the UUID of the UNKNOWN metric type.""" + result = db_session.execute(text("SELECT get_default_uuid_for('metric_type')")).scalar() + expected = db_session.query(DBMetricType.uuid).filter_by(name=METRICS.UNKNOWN.name).scalar() + assert result == expected + + +def test_get_default_uuid_for_reference(db_session): + """Stub function returns the UUID of the default UNKNOWN reference row.""" + result = db_session.execute(text("SELECT get_default_uuid_for('reference')")).scalar() + unknown_ref_type = db_session.query(DBReferenceType).filter_by(name=REF_TYPES.UNKNOWN.name).one() + expected = db_session.query(DBReference.uuid).filter_by( + reference_type_uuid=unknown_ref_type.uuid, + compound_reference_uuid=None, + ).scalar() + assert result == expected + + +def test_seeded_uuids_fixture(seeded_uuids): + """seeded_uuids convenience fixture has the expected keys.""" + assert f"metric_type.{METRICS.UNKNOWN.name}" in seeded_uuids + assert f"metric_type.{METRICS.PHASE_OFFSET.name}" in seeded_uuids + assert f"reference_type.{REF_TYPES.UNKNOWN.name}" in seeded_uuids + assert "reference.unknown" in seeded_uuids + assert "default.metric_type" in seeded_uuids + assert "default.reference" in seeded_uuids + + +def test_session_rollback_isolation(db_session, db_engine): + """Writes in one session do not leak — the savepoint rolls back cleanly.""" + from opensampl.db.orm import TestMetadata + + db_session.add(TestMetadata(name="rollback-canary")) + db_session.flush() + + # Row is visible within this session + assert db_session.query(TestMetadata).filter_by(name="rollback-canary").one() + + # After the test ends the fixture rolls back, but we can verify the + # mechanism works by checking a fresh session sees nothing yet + from sqlalchemy.orm import Session + with Session(bind=db_engine) as fresh: + result = fresh.query(TestMetadata).filter_by(name="rollback-canary").first() + assert result is None, "Savepoint did not isolate the write from other sessions" \ No newline at end of file diff --git a/tests/test_geolocator.py b/tests/test_geolocator.py new file mode 100644 index 0000000..a112503 --- /dev/null +++ b/tests/test_geolocator.py @@ -0,0 +1,127 @@ +"""Tests for geolocation helper behavior.""" + +from __future__ import annotations + +import json +from types import SimpleNamespace +from unittest.mock import Mock, patch + +from opensampl.helpers import geolocator + + +class TestLookupGeoIpApi: + """Tests for the ip-api lookup helper.""" + + def test_lookup_geo_ipapi_success_is_cached(self): + """Successful lookups should be cached and return a stable tuple.""" + body = {"status": "success", "lat": 35.9, "lon": -84.3, "city": "Oak Ridge", "country": "United States"} + + class Response: + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): # noqa: ANN001,ARG002 + return False + + def read(self): + return json.dumps(body).encode("utf-8") + + geolocator._GEO_CACHE.clear() + with patch("opensampl.helpers.geolocator.urllib.request.urlopen", return_value=Response()) as mock_urlopen: + first = geolocator._lookup_geo_ipapi("8.8.8.8") + second = geolocator._lookup_geo_ipapi("8.8.8.8") + + assert first == (35.9, -84.3, "Oak Ridge, United States") + assert second == first + mock_urlopen.assert_called_once() + + def test_lookup_geo_ipapi_failure_returns_none(self): + """Lookup failures should degrade to None instead of raising.""" + geolocator._GEO_CACHE.clear() + with patch("opensampl.helpers.geolocator.urllib.request.urlopen", side_effect=OSError("blocked")): + assert geolocator._lookup_geo_ipapi("8.8.8.8") is None + + +class TestCreateLocation: + """Tests for location creation decisions.""" + + def test_create_location_uses_override_without_network_lookup(self): + """Explicit overrides should be written directly.""" + fake_loc = SimpleNamespace(uuid="loc-123") + fake_factory = Mock() + fake_factory.find_existing.return_value = None + fake_factory.write.return_value = fake_loc + + with patch("opensampl.helpers.geolocator.TableFactory", return_value=fake_factory): + result = geolocator.create_location( + session=Mock(), + geolocate_enabled=False, + ip_address="mock-ntp-a", + geo_override={"name": "Docker lab", "lat": 37.37, "lon": -122.05}, + ) + + assert result == "loc-123" + fake_factory.write.assert_called_once_with( + {"name": "Docker lab", "lat": 37.37, "lon": -122.05, "public": True}, + if_exists="ignore", + ) + + def test_create_location_uses_existing_named_location(self): + """Named overrides should reuse existing locations when present.""" + fake_factory = Mock() + fake_factory.find_existing.return_value = SimpleNamespace(uuid="loc-existing") + + with patch("opensampl.helpers.geolocator.TableFactory", return_value=fake_factory): + result = geolocator.create_location( + session=Mock(), + geolocate_enabled=False, + ip_address="mock-ntp-b", + geo_override={"name": "Docker lab", "lat": 37.38, "lon": -122.06}, + ) + + assert result == "loc-existing" + fake_factory.write.assert_not_called() + + def test_create_location_uses_public_lookup_when_enabled(self): + """Public hosts should use lookup-derived coordinates and label when enabled.""" + fake_loc = SimpleNamespace(uuid="loc-public") + fake_factory = Mock() + fake_factory.find_existing.return_value = None + fake_factory.write.return_value = fake_loc + + with ( + patch("opensampl.helpers.geolocator.TableFactory", return_value=fake_factory), + patch("opensampl.helpers.geolocator.socket.gethostbyname", return_value="8.8.8.8"), + patch("opensampl.helpers.geolocator._lookup_geo_ipapi", return_value=(40.71, -74.0, "New York, United States")), + ): + result = geolocator.create_location( + session=Mock(), + geolocate_enabled=True, + ip_address="time.example.com", + geo_override={}, + ) + + assert result == "loc-public" + fake_factory.write.assert_called_once_with( + {"name": "New York, United States", "lat": 40.71, "lon": -74.0, "public": True}, + if_exists="ignore", + ) + + def test_create_location_returns_none_when_name_is_unavailable(self): + """Private/loopback lookups without a name should skip location creation.""" + fake_factory = Mock() + fake_factory.find_existing.return_value = None + + with ( + patch("opensampl.helpers.geolocator.TableFactory", return_value=fake_factory), + patch("opensampl.helpers.geolocator.socket.gethostbyname", return_value="127.0.0.1"), + ): + result = geolocator.create_location( + session=Mock(), + geolocate_enabled=True, + ip_address="localhost", + geo_override={}, + ) + + assert result is None + fake_factory.write.assert_not_called() diff --git a/tests/test_load_data.py b/tests/test_load_data.py index 2e6dfaf..bd6c9fb 100644 --- a/tests/test_load_data.py +++ b/tests/test_load_data.py @@ -299,6 +299,59 @@ def test_real_session_flow( assert entry.probe_id == sample_probe_key.probe_id assert entry.ip_address == sample_probe_key.ip_address + def test_ntp_metadata_creates_probe_and_vendor_rows( + self, + mock_config: Mock, + mock_session: Session, + test_db: MockDB, + mock_table_factory_with_mockdb: Any, + ): # noqa: ARG002 + """NTP metadata loading should create both probe_metadata and ntp_metadata records.""" + from opensampl.vendors.constants import ProbeKey, VENDORS + + mock_config.return_value.ENABLE_GEOLOCATE = False + probe_key = ProbeKey(probe_id="public-time", ip_address="time.cloudflare.com") + payload = { + "mode": "remote", + "reference": False, + "target_host": "time.cloudflare.com", + "target_port": 123, + "sync_status": "synchronized", + "leap_status": "no_warning", + "reference_id": "GPS", + "observation_sources": ["ntplib"], + "collection_id": "collector-host", + "collection_ip": "10.0.0.5", + "timeout": 1.5, + "additional_metadata": {"version": 3}, + } + + result = load_probe_metadata( + vendor=VENDORS.NTP, + probe_key=probe_key, + data=payload.copy(), + session=mock_session, + ) + + ProbeMetadata = test_db.table_mappings["ProbeMetadata"] # noqa: N806 + NtpMetadata = test_db.table_mappings["NtpMetadata"] # noqa: N806 + + assert result is None + + probe_row = mock_session.query(ProbeMetadata).filter_by(ip_address="time.cloudflare.com").one() + ntp_row = mock_session.query(NtpMetadata).filter_by(probe_uuid=probe_row.uuid).one() + + assert probe_row.vendor == "NTP" + assert probe_row.probe_id == "public-time" + assert probe_row.ip_address == "time.cloudflare.com" + assert ntp_row.probe_uuid == probe_row.uuid + assert ntp_row.mode == "remote" + assert ntp_row.target_host == "time.cloudflare.com" + assert ntp_row.target_port == 123 + assert ntp_row.sync_status == "synchronized" + assert ntp_row.collection_id == "collector-host" + assert ntp_row.collection_ip == "10.0.0.5" + class TestMockDb: """Tests for the mock database itself""" diff --git a/tests/test_ntp.py b/tests/test_ntp.py new file mode 100644 index 0000000..00a2eff --- /dev/null +++ b/tests/test_ntp.py @@ -0,0 +1,193 @@ +"""Tests for the NTP probe and collectors.""" + +from pathlib import Path +from types import ModuleType, SimpleNamespace +from unittest.mock import call, patch + +import pytest + +from opensampl.vendors.constants import ProbeKey +from opensampl.vendors.ntp import NTPLocalCollector, NTPRemoteCollector, NtpProbe + + +class TestNTPLocalCollector: + """Tests for local NTP collection helpers.""" + + def test_collect_prefers_chrony_outputs(self): + """Local collection should parse chrony output into probe metrics.""" + collector = NTPLocalCollector( + target_host="127.0.0.1", + collection_id="collector-host", + collection_ip="10.0.0.5", + ) + + outputs = { + ("chronyc", "tracking"): """ +Reference ID : GPS (time.cloudflare.com) +Stratum : 2 +System time : 0.000000100 seconds slow of NTP time +Last offset : +0.000000123 seconds +RMS offset : 0.000001234 seconds +Leap status : Normal +""", + ("chronyc", "sources", "-v"): """ +MS Name/IP address Stratum Poll Reach LastRx Last sample +=============================================================================== +* time.cloudflare.com 2 6 34 377 0.001 0.002 0.008 +""", + ("timedatectl", "show-timesync", "--all"): "System clock synchronized=yes\n", + ("systemctl", "show", "systemd-timesyncd", "--property=ActiveState"): "ActiveState=active\n", + } + + with patch.object( + collector, + "_run", + side_effect=lambda cmd, timeout=8.0: outputs.get(tuple(cmd)), # noqa: ARG005 + ): + collector.collect() + + assert collector.offset_s == pytest.approx(1.23e-7) + assert collector.jitter_s == pytest.approx(1.234e-6) + assert collector.stratum == 2 + assert collector.reachability == 377 + assert collector.reference_id == "time.cloudflare.com" + assert collector.sync_status == "tracking" + assert collector.sync_health == 1.0 + assert collector.probe_id == "ntp-local" + assert collector.observation_sources == [ + "chronyc_tracking", + "chronyc_sources", + "timedatectl", + "systemctl_timesyncd", + ] + + +class TestNTPRemoteCollector: + """Tests for remote NTP collection helpers.""" + + def test_collect_success_sets_metrics(self, monkeypatch: pytest.MonkeyPatch): + """Remote collection should map ntplib responses onto collector fields.""" + response = SimpleNamespace( + leap=0, + stratum=3, + poll=4, + root_delay=0.125, + root_dispersion=0.2, + delay=0.05, + offset=-0.002, + ref_id=b"GPS", + version=3, + ) + + client = SimpleNamespace(request=lambda *args, **kwargs: response) + ntplib_mod = ModuleType("ntplib") + ntplib_mod.NTPClient = lambda: client + monkeypatch.setitem(__import__("sys").modules, "ntplib", ntplib_mod) + + collector = NTPRemoteCollector( + target_host="time.cloudflare.com", + target_port=123, + timeout=1.5, + collection_id="collector-host", + collection_ip="10.0.0.5", + ) + + collector.collect() + + assert collector.sync_status == "synchronized" + assert collector.sync_health == 1.0 + assert collector.stratum == 3 + assert collector.poll_interval_s == 16.0 + assert collector.root_delay_s == pytest.approx(0.125) + assert collector.root_dispersion_s == pytest.approx(0.2) + assert collector.delay_s == pytest.approx(0.05) + assert collector.offset_s == pytest.approx(-0.002) + assert collector.jitter_s == pytest.approx((0.05 * 0.05) + (0.25 * 0.2)) + assert collector.reference_id == "GPS" + assert collector.leap_status == "no_warning" + assert collector.probe_id == "remote:123" + assert collector.observation_sources == ["ntplib"] + assert collector.extras["version"] == 3 + + def test_collect_failure_marks_probe_unreachable(self, monkeypatch: pytest.MonkeyPatch): + """Remote collection should downgrade status cleanly on request errors.""" + + class FailingClient: + def request(self, *args, **kwargs): # noqa: ARG002 + raise TimeoutError("timed out") + + ntplib_mod = ModuleType("ntplib") + ntplib_mod.NTPClient = FailingClient + monkeypatch.setitem(__import__("sys").modules, "ntplib", ntplib_mod) + + collector = NTPRemoteCollector( + target_host="time.cloudflare.com", + target_port=123, + collection_id="collector-host", + collection_ip="10.0.0.5", + ) + + collector.collect() + + assert collector.sync_status == "unreachable" + assert collector.sync_health == 0 + assert collector.observation_sources == ["ntplib", "error"] + assert collector.extras["error"] == "timed out" + + +class TestNtpProbe: + """Tests for the NTP probe parser.""" + + def test_process_metadata_sets_collection_and_target_probes(self, tmp_path: Path): + """Processing file headers should register the collection probe and target probe.""" + ntp_file = tmp_path / "sample-ntp.csv" + ntp_file.write_text( + "\n".join( + [ + "# collection_ip: 10.0.0.5", + "# collection_id: collector-host", + "# target_host: time.cloudflare.com", + "# probe_id: public-time", + "# mode: remote", + "time,value,metric", + "2026-04-24T00:00:00Z,-0.001,phase_offset_s", + ] + ) + + "\n" + ) + + with patch("opensampl.vendors.ntp.load_probe_metadata") as mock_load_probe_metadata: + probe = NtpProbe(ntp_file) + metadata = probe.process_metadata() + + assert metadata["target_host"] == "time.cloudflare.com" + assert probe.collection_probe == ProbeKey(ip_address="10.0.0.5", probe_id="collector-host") + assert probe.probe_key == ProbeKey(ip_address="time.cloudflare.com", probe_id="public-time") + mock_load_probe_metadata.assert_called_once_with( + vendor=probe.vendor, + probe_key=ProbeKey(ip_address="10.0.0.5", probe_id="collector-host"), + data={"reference": True}, + ) + + def test_load_metadata_registers_collection_probe_then_target_probe(self): + """Metadata loading should create the collection reference before the target probe.""" + metadata = { + "collection_ip": "10.0.0.5", + "collection_id": "collector-host", + "target_host": "time.cloudflare.com", + "probe_id": "public-time", + "mode": "remote", + } + probe_key = ProbeKey(ip_address="time.cloudflare.com", probe_id="public-time") + + with patch("opensampl.vendors.ntp.load_probe_metadata") as mock_load_probe_metadata: + NtpProbe.load_metadata(probe_key=probe_key, metadata=metadata) + + assert mock_load_probe_metadata.call_args_list == [ + call( + vendor=NtpProbe.vendor, + probe_key=ProbeKey(ip_address="10.0.0.5", probe_id="collector-host"), + data={"reference": True}, + ), + call(vendor=NtpProbe.vendor, probe_key=probe_key, data=metadata), + ] diff --git a/tests/test_vendors.py b/tests/test_vendors.py index bb17e89..40d8e9c 100644 --- a/tests/test_vendors.py +++ b/tests/test_vendors.py @@ -141,9 +141,6 @@ def process_time_data(self): import pandas as pd return pd.DataFrame() - def generate_random_data(self, config: BaseProbe.RandomDataConfig, probe_key: ProbeKey): - return probe_key - probe = TestProbe("test_file.txt") assert probe.input_file == Path("test_file.txt") @@ -164,9 +161,6 @@ def process_time_data(self): import pandas as pd return pd.DataFrame() - def generate_random_data(self, config: BaseProbe.RandomDataConfig, probe_key: ProbeKey): - return probe_key - # Mock the click context mock_ctx = Mock() mock_ctx.obj = {"conf": Mock()} @@ -203,9 +197,6 @@ def process_time_data(self): import pandas as pd return pd.DataFrame() - def generate_random_data(self, config: BaseProbe.RandomDataConfig, probe_key: ProbeKey): - return probe_key - mock_ctx = Mock() mock_ctx.obj = {"conf": Mock()} mock_ctx.obj["conf"].ARCHIVE_PATH = Path("/default/archive") @@ -247,9 +238,6 @@ def process_time_data(self): import pandas as pd return pd.DataFrame() - def generate_random_data(self, config: BaseProbe.RandomDataConfig, probe_key: ProbeKey): - return probe_key - mock_ctx = Mock() mock_ctx.obj = {"conf": Mock()} mock_ctx.obj["conf"].ARCHIVE_PATH = Path("/default/archive") @@ -346,4 +334,4 @@ def test_vendors_get_by_name_case_insensitive(self): def test_vendors_get_by_name_not_found(self): """Test getting vendor by name that doesn't exist.""" with pytest.raises(ValueError): - VENDORS.get_by_name("NONEXISTENT") \ No newline at end of file + VENDORS.get_by_name("NONEXISTENT") diff --git a/tests/utils/mockdb.py b/tests/utils/mockdb.py index 44e27f1..58b5e1c 100644 --- a/tests/utils/mockdb.py +++ b/tests/utils/mockdb.py @@ -499,14 +499,14 @@ def _load_metric_types(self, session: Session) -> str: # Get all metric types from the METRICS class metrics = [attr for attr in METRICS.__dict__.values() if isinstance(attr, MetricType)] - phaseerr_uuid = None + unknown_uuid = None for m in metrics: metric = MetricTypeTable(**m.model_dump()) session.add(metric) session.flush() - if metric.name == "PHASE": - phaseerr_uuid = metric.uuid - return phaseerr_uuid + if metric.name == "UNKNOWN": + unknown_uuid = metric.uuid + return unknown_uuid def _load_reference_types(self, session: Session) -> str: """Load reference types from opensampl.references.REF_TYPES and return the UNKNOWN one's UUID.""" diff --git a/tox.ini b/tox.ini index 838288c..ebb6afa 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py39, py310, py311, py312, lint +envlist = py310, py311, py312, py313, py314, lint skipsdist = True isolated_build = True @@ -83,4 +83,4 @@ disable = unused-argument, missing-class-docstring, too few public methods, - missing function or method docstring \ No newline at end of file + missing function or method docstring diff --git a/uv.lock b/uv.lock index 0ba575b..48439e8 100644 --- a/uv.lock +++ b/uv.lock @@ -1,37 +1,35 @@ version = 1 -revision = 2 -requires-python = ">=3.9, <3.13" +revision = 3 +requires-python = ">=3.10, <3.15" resolution-markers = [ - "python_full_version >= '3.12'", + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", - "python_full_version < '3.10'", + "python_full_version < '3.11'", ] [[package]] name = "alabaster" -version = "0.7.16" +version = "1.0.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776, upload-time = "2024-01-10T00:56:10.189Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511, upload-time = "2024-01-10T00:56:08.388Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, ] [[package]] -name = "alabaster" -version = "1.0.0" +name = "alembic" +version = "1.18.4" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +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/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, + { 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]] @@ -39,19 +37,26 @@ name = "allantools" version = "2024.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "matplotlib", version = "3.10.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "matplotlib" }, { name = "numpy" }, { name = "numpydoc" }, { name = "pytest" }, - { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "scipy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ab/81/1adc1ffe918959f3df124aef8e1dde380a6d2ce24528c5da8107596b3e02/allantools-2024.6.tar.gz", hash = "sha256:c4380c74de834ac869aefc899038e784ef1dd396370be89d6836abffbe484289", size = 4093089, upload-time = "2024-07-04T06:45:16.489Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e4/a1/d32722ff0475739230c28d3f1194cf16d360d4b328ff590f8ea4a00e6fca/allantools-2024.6-py3-none-any.whl", hash = "sha256:0d0d20e3c45245c4aff5346c59ae0f6e56ed61e691e481a06ffbab94875df2c9", size = 47275, upload-time = "2024-07-04T06:45:10Z" }, ] +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -61,6 +66,20 @@ 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 = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { 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 = "astor" version = "0.8.1" @@ -88,6 +107,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bf/cb/d019ab87fe70e0fe3946196d50d6a4428623dc0c38a6669c8cae0320fbf3/backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d", size = 380337, upload-time = "2025-02-25T16:53:14.607Z" }, { url = "https://files.pythonhosted.org/packages/a9/86/abd17f50ee21b2248075cb6924c6e7f9d23b4925ca64ec660e869c2633f1/backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b", size = 392142, upload-time = "2025-02-25T16:53:17.266Z" }, { url = "https://files.pythonhosted.org/packages/b3/04/7b415bd75c8ab3268cc138c76fa648c19495fcc7d155508a0e62f3f82308/backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486", size = 398021, upload-time = "2025-02-25T16:53:26.378Z" }, + { url = "https://files.pythonhosted.org/packages/04/b8/60dcfb90eb03a06e883a92abbc2ab95c71f0d8c9dd0af76ab1d5ce0b1402/backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585", size = 399915, upload-time = "2025-02-25T16:53:28.167Z" }, { url = "https://files.pythonhosted.org/packages/0c/37/fb6973edeb700f6e3d6ff222400602ab1830446c25c7b4676d8de93e65b8/backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc", size = 380336, upload-time = "2025-02-25T16:53:29.858Z" }, ] @@ -145,48 +165,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, - { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671, upload-time = "2025-05-02T08:34:12.696Z" }, - { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744, upload-time = "2025-05-02T08:34:14.665Z" }, - { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993, upload-time = "2025-05-02T08:34:17.134Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382, upload-time = "2025-05-02T08:34:19.081Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536, upload-time = "2025-05-02T08:34:21.073Z" }, - { url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349, upload-time = "2025-05-02T08:34:23.193Z" }, - { url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365, upload-time = "2025-05-02T08:34:25.187Z" }, - { url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499, upload-time = "2025-05-02T08:34:27.359Z" }, - { url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735, upload-time = "2025-05-02T08:34:29.798Z" }, - { url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786, upload-time = "2025-05-02T08:34:31.858Z" }, - { url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203, upload-time = "2025-05-02T08:34:33.88Z" }, - { url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436, upload-time = "2025-05-02T08:34:35.907Z" }, - { url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772, upload-time = "2025-05-02T08:34:37.935Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] -[[package]] -name = "click" -version = "8.1.8" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and 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" version = "8.2.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", -] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } wheels = [ @@ -202,77 +202,12 @@ 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.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "numpy", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/f6/31a8f28b4a2a4fa0e01085e542f3081ab0588eff8e589d39d775172c9792/contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4", size = 13464370, upload-time = "2024-08-27T21:00:03.328Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/e0/be8dcc796cfdd96708933e0e2da99ba4bb8f9b2caa9d560a50f3f09a65f3/contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7", size = 265366, upload-time = "2024-08-27T20:50:09.947Z" }, - { url = "https://files.pythonhosted.org/packages/50/d6/c953b400219443535d412fcbbc42e7a5e823291236bc0bb88936e3cc9317/contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42", size = 249226, upload-time = "2024-08-27T20:50:16.1Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b4/6fffdf213ffccc28483c524b9dad46bb78332851133b36ad354b856ddc7c/contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7", size = 308460, upload-time = "2024-08-27T20:50:22.536Z" }, - { url = "https://files.pythonhosted.org/packages/cf/6c/118fc917b4050f0afe07179a6dcbe4f3f4ec69b94f36c9e128c4af480fb8/contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab", size = 347623, upload-time = "2024-08-27T20:50:28.806Z" }, - { url = "https://files.pythonhosted.org/packages/f9/a4/30ff110a81bfe3abf7b9673284d21ddce8cc1278f6f77393c91199da4c90/contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589", size = 317761, upload-time = "2024-08-27T20:50:35.126Z" }, - { url = "https://files.pythonhosted.org/packages/99/e6/d11966962b1aa515f5586d3907ad019f4b812c04e4546cc19ebf62b5178e/contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41", size = 322015, upload-time = "2024-08-27T20:50:40.318Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e3/182383743751d22b7b59c3c753277b6aee3637049197624f333dac5b4c80/contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d", size = 1262672, upload-time = "2024-08-27T20:50:55.643Z" }, - { url = "https://files.pythonhosted.org/packages/78/53/974400c815b2e605f252c8fb9297e2204347d1755a5374354ee77b1ea259/contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223", size = 1321688, upload-time = "2024-08-27T20:51:11.293Z" }, - { url = "https://files.pythonhosted.org/packages/52/29/99f849faed5593b2926a68a31882af98afbeac39c7fdf7de491d9c85ec6a/contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f", size = 171145, upload-time = "2024-08-27T20:51:15.2Z" }, - { url = "https://files.pythonhosted.org/packages/a9/97/3f89bba79ff6ff2b07a3cbc40aa693c360d5efa90d66e914f0ff03b95ec7/contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b", size = 216019, upload-time = "2024-08-27T20:51:19.365Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1f/9375917786cb39270b0ee6634536c0e22abf225825602688990d8f5c6c19/contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad", size = 266356, upload-time = "2024-08-27T20:51:24.146Z" }, - { url = "https://files.pythonhosted.org/packages/05/46/9256dd162ea52790c127cb58cfc3b9e3413a6e3478917d1f811d420772ec/contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49", size = 250915, upload-time = "2024-08-27T20:51:28.683Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5d/3056c167fa4486900dfbd7e26a2fdc2338dc58eee36d490a0ed3ddda5ded/contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66", size = 310443, upload-time = "2024-08-27T20:51:33.675Z" }, - { url = "https://files.pythonhosted.org/packages/ca/c2/1a612e475492e07f11c8e267ea5ec1ce0d89971be496c195e27afa97e14a/contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081", size = 348548, upload-time = "2024-08-27T20:51:39.322Z" }, - { url = "https://files.pythonhosted.org/packages/45/cf/2c2fc6bb5874158277b4faf136847f0689e1b1a1f640a36d76d52e78907c/contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1", size = 319118, upload-time = "2024-08-27T20:51:44.717Z" }, - { url = "https://files.pythonhosted.org/packages/03/33/003065374f38894cdf1040cef474ad0546368eea7e3a51d48b8a423961f8/contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d", size = 323162, upload-time = "2024-08-27T20:51:49.683Z" }, - { url = "https://files.pythonhosted.org/packages/42/80/e637326e85e4105a802e42959f56cff2cd39a6b5ef68d5d9aee3ea5f0e4c/contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c", size = 1265396, upload-time = "2024-08-27T20:52:04.926Z" }, - { url = "https://files.pythonhosted.org/packages/7c/3b/8cbd6416ca1bbc0202b50f9c13b2e0b922b64be888f9d9ee88e6cfabfb51/contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb", size = 1324297, upload-time = "2024-08-27T20:52:21.843Z" }, - { url = "https://files.pythonhosted.org/packages/4d/2c/021a7afaa52fe891f25535506cc861c30c3c4e5a1c1ce94215e04b293e72/contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c", size = 171808, upload-time = "2024-08-27T20:52:25.163Z" }, - { url = "https://files.pythonhosted.org/packages/8d/2f/804f02ff30a7fae21f98198828d0857439ec4c91a96e20cf2d6c49372966/contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67", size = 217181, upload-time = "2024-08-27T20:52:29.13Z" }, - { url = "https://files.pythonhosted.org/packages/c9/92/8e0bbfe6b70c0e2d3d81272b58c98ac69ff1a4329f18c73bd64824d8b12e/contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f", size = 267838, upload-time = "2024-08-27T20:52:33.911Z" }, - { url = "https://files.pythonhosted.org/packages/e3/04/33351c5d5108460a8ce6d512307690b023f0cfcad5899499f5c83b9d63b1/contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6", size = 251549, upload-time = "2024-08-27T20:52:39.179Z" }, - { url = "https://files.pythonhosted.org/packages/51/3d/aa0fe6ae67e3ef9f178389e4caaaa68daf2f9024092aa3c6032e3d174670/contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639", size = 303177, upload-time = "2024-08-27T20:52:44.789Z" }, - { url = "https://files.pythonhosted.org/packages/56/c3/c85a7e3e0cab635575d3b657f9535443a6f5d20fac1a1911eaa4bbe1aceb/contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c", size = 341735, upload-time = "2024-08-27T20:52:51.05Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8d/20f7a211a7be966a53f474bc90b1a8202e9844b3f1ef85f3ae45a77151ee/contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06", size = 314679, upload-time = "2024-08-27T20:52:58.473Z" }, - { url = "https://files.pythonhosted.org/packages/6e/be/524e377567defac0e21a46e2a529652d165fed130a0d8a863219303cee18/contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09", size = 320549, upload-time = "2024-08-27T20:53:06.593Z" }, - { url = "https://files.pythonhosted.org/packages/0f/96/fdb2552a172942d888915f3a6663812e9bc3d359d53dafd4289a0fb462f0/contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd", size = 1263068, upload-time = "2024-08-27T20:53:23.442Z" }, - { url = "https://files.pythonhosted.org/packages/2a/25/632eab595e3140adfa92f1322bf8915f68c932bac468e89eae9974cf1c00/contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35", size = 1322833, upload-time = "2024-08-27T20:53:39.243Z" }, - { url = "https://files.pythonhosted.org/packages/73/e3/69738782e315a1d26d29d71a550dbbe3eb6c653b028b150f70c1a5f4f229/contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb", size = 172681, upload-time = "2024-08-27T20:53:43.05Z" }, - { url = "https://files.pythonhosted.org/packages/0c/89/9830ba00d88e43d15e53d64931e66b8792b46eb25e2050a88fec4a0df3d5/contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b", size = 218283, upload-time = "2024-08-27T20:53:47.232Z" }, - { url = "https://files.pythonhosted.org/packages/b3/e3/b9f72758adb6ef7397327ceb8b9c39c75711affb220e4f53c745ea1d5a9a/contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8", size = 265518, upload-time = "2024-08-27T20:56:01.333Z" }, - { url = "https://files.pythonhosted.org/packages/ec/22/19f5b948367ab5260fb41d842c7a78dae645603881ea6bc39738bcfcabf6/contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c", size = 249350, upload-time = "2024-08-27T20:56:05.432Z" }, - { url = "https://files.pythonhosted.org/packages/26/76/0c7d43263dd00ae21a91a24381b7e813d286a3294d95d179ef3a7b9fb1d7/contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca", size = 309167, upload-time = "2024-08-27T20:56:10.034Z" }, - { url = "https://files.pythonhosted.org/packages/96/3b/cadff6773e89f2a5a492c1a8068e21d3fccaf1a1c1df7d65e7c8e3ef60ba/contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f", size = 348279, upload-time = "2024-08-27T20:56:15.41Z" }, - { url = "https://files.pythonhosted.org/packages/e1/86/158cc43aa549d2081a955ab11c6bdccc7a22caacc2af93186d26f5f48746/contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc", size = 318519, upload-time = "2024-08-27T20:56:21.813Z" }, - { url = "https://files.pythonhosted.org/packages/05/11/57335544a3027e9b96a05948c32e566328e3a2f84b7b99a325b7a06d2b06/contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2", size = 321922, upload-time = "2024-08-27T20:56:26.983Z" }, - { url = "https://files.pythonhosted.org/packages/0b/e3/02114f96543f4a1b694333b92a6dcd4f8eebbefcc3a5f3bbb1316634178f/contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e", size = 1258017, upload-time = "2024-08-27T20:56:42.246Z" }, - { url = "https://files.pythonhosted.org/packages/f3/3b/bfe4c81c6d5881c1c643dde6620be0b42bf8aab155976dd644595cfab95c/contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800", size = 1316773, upload-time = "2024-08-27T20:56:58.58Z" }, - { url = "https://files.pythonhosted.org/packages/f1/17/c52d2970784383cafb0bd918b6fb036d98d96bbf0bc1befb5d1e31a07a70/contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5", size = 171353, upload-time = "2024-08-27T20:57:02.718Z" }, - { url = "https://files.pythonhosted.org/packages/53/23/db9f69676308e094d3c45f20cc52e12d10d64f027541c995d89c11ad5c75/contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843", size = 211817, upload-time = "2024-08-27T20:57:06.328Z" }, - { url = "https://files.pythonhosted.org/packages/d1/09/60e486dc2b64c94ed33e58dcfb6f808192c03dfc5574c016218b9b7680dc/contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c", size = 261886, upload-time = "2024-08-27T20:57:10.863Z" }, - { url = "https://files.pythonhosted.org/packages/19/20/b57f9f7174fcd439a7789fb47d764974ab646fa34d1790551de386457a8e/contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779", size = 311008, upload-time = "2024-08-27T20:57:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/74/fc/5040d42623a1845d4f17a418e590fd7a79ae8cb2bad2b2f83de63c3bdca4/contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4", size = 215690, upload-time = "2024-08-27T20:57:19.321Z" }, - { url = "https://files.pythonhosted.org/packages/2b/24/dc3dcd77ac7460ab7e9d2b01a618cb31406902e50e605a8d6091f0a8f7cc/contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0", size = 261894, upload-time = "2024-08-27T20:57:23.873Z" }, - { url = "https://files.pythonhosted.org/packages/b1/db/531642a01cfec39d1682e46b5457b07cf805e3c3c584ec27e2a6223f8f6c/contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102", size = 311099, upload-time = "2024-08-27T20:57:28.58Z" }, - { url = "https://files.pythonhosted.org/packages/38/1e/94bda024d629f254143a134eead69e21c836429a2a6ce82209a00ddcb79a/contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb", size = 215838, upload-time = "2024-08-27T20:57:32.913Z" }, -] - [[package]] name = "contourpy" version = "1.3.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", -] dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.10'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } wheels = [ @@ -306,6 +241,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, @@ -352,16 +307,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/fa/f3e7ec7d220bff14aba7a4786ae47043770cbdceeea1803083059c878837/coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8", size = 214366, upload-time = "2025-05-23T11:38:43.551Z" }, { url = "https://files.pythonhosted.org/packages/54/aa/9cbeade19b7e8e853e7ffc261df885d66bf3a782c71cba06c17df271f9e6/coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223", size = 215165, upload-time = "2025-05-23T11:38:45.148Z" }, { url = "https://files.pythonhosted.org/packages/c4/73/e2528bf1237d2448f882bbebaec5c3500ef07301816c5c63464b9da4d88a/coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f", size = 213548, upload-time = "2025-05-23T11:38:46.74Z" }, - { url = "https://files.pythonhosted.org/packages/71/1e/388267ad9c6aa126438acc1ceafede3bb746afa9872e3ec5f0691b7d5efa/coverage-7.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:496948261eaac5ac9cf43f5d0a9f6eb7a6d4cb3bedb2c5d294138142f5c18f2a", size = 211566, upload-time = "2025-05-23T11:39:32.333Z" }, - { url = "https://files.pythonhosted.org/packages/8f/a5/acc03e5cf0bba6357f5e7c676343de40fbf431bb1e115fbebf24b2f7f65e/coverage-7.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eacd2de0d30871eff893bab0b67840a96445edcb3c8fd915e6b11ac4b2f3fa6d", size = 211996, upload-time = "2025-05-23T11:39:34.512Z" }, - { url = "https://files.pythonhosted.org/packages/5b/a2/0fc0a9f6b7c24fa4f1d7210d782c38cb0d5e692666c36eaeae9a441b6755/coverage-7.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b039ffddc99ad65d5078ef300e0c7eed08c270dc26570440e3ef18beb816c1ca", size = 240741, upload-time = "2025-05-23T11:39:36.252Z" }, - { url = "https://files.pythonhosted.org/packages/e6/da/1c6ba2cf259710eed8916d4fd201dccc6be7380ad2b3b9f63ece3285d809/coverage-7.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e49824808d4375ede9dd84e9961a59c47f9113039f1a525e6be170aa4f5c34d", size = 238672, upload-time = "2025-05-23T11:39:38.03Z" }, - { url = "https://files.pythonhosted.org/packages/ac/51/c8fae0dc3ca421e6e2509503696f910ff333258db672800c3bdef256265a/coverage-7.8.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b069938961dfad881dc2f8d02b47645cd2f455d3809ba92a8a687bf513839787", size = 239769, upload-time = "2025-05-23T11:39:40.24Z" }, - { url = "https://files.pythonhosted.org/packages/59/8e/b97042ae92c59f40be0c989df090027377ba53f2d6cef73c9ca7685c26a6/coverage-7.8.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:de77c3ba8bb686d1c411e78ee1b97e6e0b963fb98b1637658dd9ad2c875cf9d7", size = 239555, upload-time = "2025-05-23T11:39:42.3Z" }, - { url = "https://files.pythonhosted.org/packages/47/35/b8893e682d6e96b1db2af5997fc13ef62219426fb17259d6844c693c5e00/coverage-7.8.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1676628065a498943bd3f64f099bb573e08cf1bc6088bbe33cf4424e0876f4b3", size = 237768, upload-time = "2025-05-23T11:39:44.069Z" }, - { url = "https://files.pythonhosted.org/packages/03/6c/023b0b9a764cb52d6243a4591dcb53c4caf4d7340445113a1f452bb80591/coverage-7.8.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8e1a26e7e50076e35f7afafde570ca2b4d7900a491174ca357d29dece5aacee7", size = 238757, upload-time = "2025-05-23T11:39:46.195Z" }, - { url = "https://files.pythonhosted.org/packages/03/ed/3af7e4d721bd61a8df7de6de9e8a4271e67f3d9e086454558fd9f48eb4f6/coverage-7.8.2-cp39-cp39-win32.whl", hash = "sha256:6782a12bf76fa61ad9350d5a6ef5f3f020b57f5e6305cbc663803f2ebd0f270a", size = 214166, upload-time = "2025-05-23T11:39:47.934Z" }, - { url = "https://files.pythonhosted.org/packages/9d/30/ee774b626773750dc6128354884652507df3c59d6aa8431526107e595227/coverage-7.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:1efa4166ba75ccefd647f2d78b64f53f14fb82622bc94c5a5cb0a622f50f1c9e", size = 215050, upload-time = "2025-05-23T11:39:50.252Z" }, + { url = "https://files.pythonhosted.org/packages/1a/93/eb6400a745ad3b265bac36e8077fdffcf0268bdbbb6c02b7220b624c9b31/coverage-7.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea561010914ec1c26ab4188aef8b1567272ef6de096312716f90e5baa79ef8ca", size = 211898, upload-time = "2025-05-23T11:38:49.066Z" }, + { url = "https://files.pythonhosted.org/packages/1b/7c/bdbf113f92683024406a1cd226a199e4200a2001fc85d6a6e7e299e60253/coverage-7.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cb86337a4fcdd0e598ff2caeb513ac604d2f3da6d53df2c8e368e07ee38e277d", size = 212171, upload-time = "2025-05-23T11:38:51.207Z" }, + { url = "https://files.pythonhosted.org/packages/91/22/594513f9541a6b88eb0dba4d5da7d71596dadef6b17a12dc2c0e859818a9/coverage-7.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a4636ddb666971345541b59899e969f3b301143dd86b0ddbb570bd591f1e85", size = 245564, upload-time = "2025-05-23T11:38:52.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f4/2860fd6abeebd9f2efcfe0fd376226938f22afc80c1943f363cd3c28421f/coverage-7.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5040536cf9b13fb033f76bcb5e1e5cb3b57c4807fef37db9e0ed129c6a094257", size = 242719, upload-time = "2025-05-23T11:38:54.529Z" }, + { url = "https://files.pythonhosted.org/packages/89/60/f5f50f61b6332451520e6cdc2401700c48310c64bc2dd34027a47d6ab4ca/coverage-7.8.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc67994df9bcd7e0150a47ef41278b9e0a0ea187caba72414b71dc590b99a108", size = 244634, upload-time = "2025-05-23T11:38:57.326Z" }, + { url = "https://files.pythonhosted.org/packages/3b/70/7f4e919039ab7d944276c446b603eea84da29ebcf20984fb1fdf6e602028/coverage-7.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e6c86888fd076d9e0fe848af0a2142bf606044dc5ceee0aa9eddb56e26895a0", size = 244824, upload-time = "2025-05-23T11:38:59.421Z" }, + { url = "https://files.pythonhosted.org/packages/26/45/36297a4c0cea4de2b2c442fe32f60c3991056c59cdc3cdd5346fbb995c97/coverage-7.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:684ca9f58119b8e26bef860db33524ae0365601492e86ba0b71d513f525e7050", size = 242872, upload-time = "2025-05-23T11:39:01.049Z" }, + { url = "https://files.pythonhosted.org/packages/a4/71/e041f1b9420f7b786b1367fa2a375703889ef376e0d48de9f5723fb35f11/coverage-7.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8165584ddedb49204c4e18da083913bdf6a982bfb558632a79bdaadcdafd0d48", size = 244179, upload-time = "2025-05-23T11:39:02.709Z" }, + { url = "https://files.pythonhosted.org/packages/bd/db/3c2bf49bdc9de76acf2491fc03130c4ffc51469ce2f6889d2640eb563d77/coverage-7.8.2-cp313-cp313-win32.whl", hash = "sha256:34759ee2c65362163699cc917bdb2a54114dd06d19bab860725f94ef45a3d9b7", size = 214393, upload-time = "2025-05-23T11:39:05.457Z" }, + { url = "https://files.pythonhosted.org/packages/c6/dc/947e75d47ebbb4b02d8babb1fad4ad381410d5bc9da7cfca80b7565ef401/coverage-7.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f9bc608fbafaee40eb60a9a53dbfb90f53cc66d3d32c2849dc27cf5638a21e3", size = 215194, upload-time = "2025-05-23T11:39:07.171Z" }, + { url = "https://files.pythonhosted.org/packages/90/31/a980f7df8a37eaf0dc60f932507fda9656b3a03f0abf188474a0ea188d6d/coverage-7.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9fe449ee461a3b0c7105690419d0b0aba1232f4ff6d120a9e241e58a556733f7", size = 213580, upload-time = "2025-05-23T11:39:08.862Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6a/25a37dd90f6c95f59355629417ebcb74e1c34e38bb1eddf6ca9b38b0fc53/coverage-7.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8369a7c8ef66bded2b6484053749ff220dbf83cba84f3398c84c51a6f748a008", size = 212734, upload-time = "2025-05-23T11:39:11.109Z" }, + { url = "https://files.pythonhosted.org/packages/36/8b/3a728b3118988725f40950931abb09cd7f43b3c740f4640a59f1db60e372/coverage-7.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:159b81df53a5fcbc7d45dae3adad554fdbde9829a994e15227b3f9d816d00b36", size = 212959, upload-time = "2025-05-23T11:39:12.751Z" }, + { url = "https://files.pythonhosted.org/packages/53/3c/212d94e6add3a3c3f412d664aee452045ca17a066def8b9421673e9482c4/coverage-7.8.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6fcbbd35a96192d042c691c9e0c49ef54bd7ed865846a3c9d624c30bb67ce46", size = 257024, upload-time = "2025-05-23T11:39:15.569Z" }, + { url = "https://files.pythonhosted.org/packages/a4/40/afc03f0883b1e51bbe804707aae62e29c4e8c8bbc365c75e3e4ddeee9ead/coverage-7.8.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05364b9cc82f138cc86128dc4e2e1251c2981a2218bfcd556fe6b0fbaa3501be", size = 252867, upload-time = "2025-05-23T11:39:17.64Z" }, + { url = "https://files.pythonhosted.org/packages/18/a2/3699190e927b9439c6ded4998941a3c1d6fa99e14cb28d8536729537e307/coverage-7.8.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d532db4e5ff3979ce47d18e2fe8ecad283eeb7367726da0e5ef88e4fe64740", size = 255096, upload-time = "2025-05-23T11:39:19.328Z" }, + { url = "https://files.pythonhosted.org/packages/b4/06/16e3598b9466456b718eb3e789457d1a5b8bfb22e23b6e8bbc307df5daf0/coverage-7.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4000a31c34932e7e4fa0381a3d6deb43dc0c8f458e3e7ea6502e6238e10be625", size = 256276, upload-time = "2025-05-23T11:39:21.077Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d5/4b5a120d5d0223050a53d2783c049c311eea1709fa9de12d1c358e18b707/coverage-7.8.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:43ff5033d657cd51f83015c3b7a443287250dc14e69910577c3e03bd2e06f27b", size = 254478, upload-time = "2025-05-23T11:39:22.838Z" }, + { url = "https://files.pythonhosted.org/packages/ba/85/f9ecdb910ecdb282b121bfcaa32fa8ee8cbd7699f83330ee13ff9bbf1a85/coverage-7.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94316e13f0981cbbba132c1f9f365cac1d26716aaac130866ca812006f662199", size = 255255, upload-time = "2025-05-23T11:39:24.644Z" }, + { url = "https://files.pythonhosted.org/packages/50/63/2d624ac7d7ccd4ebbd3c6a9eba9d7fc4491a1226071360d59dd84928ccb2/coverage-7.8.2-cp313-cp313t-win32.whl", hash = "sha256:3f5673888d3676d0a745c3d0e16da338c5eea300cb1f4ada9c872981265e76d8", size = 215109, upload-time = "2025-05-23T11:39:26.722Z" }, + { url = "https://files.pythonhosted.org/packages/22/5e/7053b71462e970e869111c1853afd642212568a350eba796deefdfbd0770/coverage-7.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:2c08b05ee8d7861e45dc5a2cc4195c8c66dca5ac613144eb6ebeaff2d502e73d", size = 216268, upload-time = "2025-05-23T11:39:28.429Z" }, + { url = "https://files.pythonhosted.org/packages/07/69/afa41aa34147655543dbe96994f8a246daf94b361ccf5edfd5df62ce066a/coverage-7.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:1e1448bb72b387755e1ff3ef1268a06617afd94188164960dba8d0245a46004b", size = 214071, upload-time = "2025-05-23T11:39:30.55Z" }, { url = "https://files.pythonhosted.org/packages/69/2f/572b29496d8234e4a7773200dd835a0d32d9e171f2d974f3fe04a9dbc271/coverage-7.8.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:ec455eedf3ba0bbdf8f5a570012617eb305c63cb9f03428d39bf544cb2b94837", size = 203636, upload-time = "2025-05-23T11:39:52.002Z" }, { url = "https://files.pythonhosted.org/packages/a0/1a/0b9c32220ad694d66062f571cc5cedfa9997b64a591e8a500bb63de1bd40/coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32", size = 203623, upload-time = "2025-05-23T11:39:53.846Z" }, ] @@ -401,6 +368,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] +[[package]] +name = "fastapi" +version = "0.136.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/d9/e66315807e41e69e7f6a1b42a162dada2f249c5f06ad3f1a95f84ab336ef/fastapi-0.136.0.tar.gz", hash = "sha256:cf08e067cc66e106e102d9ba659463abfac245200752f8a5b7b1e813de4ff73e", size = 396607, upload-time = "2026-04-16T11:47:13.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/a3/0bd5f0cdb0bbc92650e8dc457e9250358411ee5d1b65e42b6632387daf81/fastapi-0.136.0-py3-none-any.whl", hash = "sha256:8793d44ec7378e2be07f8a013cf7f7aa47d6327d0dfe9804862688ec4541a6b4", size = 117556, upload-time = "2026-04-16T11:47:11.922Z" }, +] + [[package]] name = "fonttools" version = "4.58.1" @@ -431,14 +414,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b6/7e/83b409659eb4818f1283a8319f3570497718d6d3b70f4fca2ddf962e948e/fonttools-4.58.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4db9399ee633855c718fe8bea5eecbdc5bf3fdbed2648e50f67f8946b943ed1c", size = 5026677, upload-time = "2025-05-28T15:28:45.354Z" }, { url = "https://files.pythonhosted.org/packages/34/52/1eb69802d3b54e569158c97810195f317d350f56390b83c43e1c999551d8/fonttools-4.58.1-cp312-cp312-win32.whl", hash = "sha256:5cf04c4f73d36b30ea1cff091a7a9e65f8d5b08345b950f82679034e9f7573f4", size = 2176201, upload-time = "2025-05-28T15:28:47.417Z" }, { url = "https://files.pythonhosted.org/packages/6f/25/8dcfeb771de8d9cdffab2b957a05af4395d41ec9a198ec139d2326366a07/fonttools-4.58.1-cp312-cp312-win_amd64.whl", hash = "sha256:4a3841b59c67fa1f739542b05211609c453cec5d11d21f863dd2652d5a81ec9b", size = 2225519, upload-time = "2025-05-28T15:28:49.431Z" }, - { url = "https://files.pythonhosted.org/packages/0a/b6/eaa8b2f38ad5339bc51ff75bf6a9c29e4b619453d8378ae9a374535e954d/fonttools-4.58.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:927762f9fe39ea0a4d9116353251f409389a6b58fab58717d3c3377acfc23452", size = 2740399, upload-time = "2025-05-28T15:29:07.124Z" }, - { url = "https://files.pythonhosted.org/packages/94/a1/6b56d0a5e20be9586c7669189cdcfcabced90bf676030f46397162d56926/fonttools-4.58.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:761ac80efcb7333c71760458c23f728d6fe2dff253b649faf52471fd7aebe584", size = 2309460, upload-time = "2025-05-28T15:29:08.928Z" }, - { url = "https://files.pythonhosted.org/packages/23/3c/bebd50b085d78d64ee518fb9c95fd08b90f9b715ca08c0b43fd53a963560/fonttools-4.58.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deef910226f788a4e72aa0fc1c1657fb43fa62a4200b883edffdb1392b03fe86", size = 4701742, upload-time = "2025-05-28T15:29:11.103Z" }, - { url = "https://files.pythonhosted.org/packages/89/4a/dbc6f9efac98718feba2735ceb72237e8965a4878529c0af6d33f32e7403/fonttools-4.58.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff2859ca2319454df8c26af6693269b21f2e9c0e46df126be916a4f6d85fc75", size = 4730821, upload-time = "2025-05-28T15:29:13.405Z" }, - { url = "https://files.pythonhosted.org/packages/63/ed/1a64f06747d05a8bb4d6b2bf7de59e960533d5303f254cf366cc4d827e7d/fonttools-4.58.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:418927e888e1bcc976b4e190a562f110dc27b0b5cac18033286f805dc137fc66", size = 4787238, upload-time = "2025-05-28T15:29:15.593Z" }, - { url = "https://files.pythonhosted.org/packages/86/82/ecb3e23507cca2548902cb1f1c09c7d919b9ad1bf83e464fd2c7c924adf6/fonttools-4.58.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a907007a8b341e8e129d3994d34d1cc85bc8bf38b3a0be65eb14e4668f634a21", size = 4895738, upload-time = "2025-05-28T15:29:18.022Z" }, - { url = "https://files.pythonhosted.org/packages/dc/44/73c560fbcdee65ffcf2dc9069afc21d5afab1cbdf318284d56649e937b30/fonttools-4.58.1-cp39-cp39-win32.whl", hash = "sha256:455cb6adc9f3419273925fadc51a6207046e147ce503797b29895ba6bdf85762", size = 1468967, upload-time = "2025-05-28T15:29:20.106Z" }, - { url = "https://files.pythonhosted.org/packages/70/fe/df31c80575567b7239d225760a820b3abfe307e2830a9119bd4a6eb6bb8f/fonttools-4.58.1-cp39-cp39-win_amd64.whl", hash = "sha256:2e64931258866df187bd597b4e9fff488f059a0bc230fbae434f0f112de3ce46", size = 1513516, upload-time = "2025-05-28T15:29:22.294Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/7ed2e4e381f9b1f5122d33b7e626a40f646cacc1ef72d8806aacece9e580/fonttools-4.58.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68379d1599fc59569956a97eb7b07e0413f76142ac8513fa24c9f2c03970543a", size = 2731231, upload-time = "2025-05-28T15:28:51.435Z" }, + { url = "https://files.pythonhosted.org/packages/e7/28/74864dc9248e917cbe07c903e0ce1517c89d42e2fab6b0ce218387ef0e24/fonttools-4.58.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8631905657de4f9a7ae1e12186c1ed20ba4d6168c2d593b9e0bd2908061d341b", size = 2305224, upload-time = "2025-05-28T15:28:53.114Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f1/ced758896188c1632c5b034a0741457f305e087eb4fa762d86aa3c1ae422/fonttools-4.58.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2ecea7289061c2c71468723409a8dd6e70d1ecfce6bc7686e5a74b9ce9154fe", size = 4793934, upload-time = "2025-05-28T15:28:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/c1/46/8b46469c6edac393de1c380c7ec61922d5440f25605dfca7849e5ffff295/fonttools-4.58.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b8860f8cd48b345bd1df1d7be650f600f69ee971ffe338c5bd5bcb6bdb3b92c", size = 4863415, upload-time = "2025-05-28T15:28:56.917Z" }, + { url = "https://files.pythonhosted.org/packages/12/1b/82aa678bb96af6663fe163d51493ffb8622948f4908c886cba6b67fbf6c5/fonttools-4.58.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7c9a0acdefcb8d7ccd7c59202056166c400e797047009ecb299b75ab950c2a9c", size = 4865025, upload-time = "2025-05-28T15:28:58.926Z" }, + { url = "https://files.pythonhosted.org/packages/7d/26/b66ab2f2dc34b962caecd6fa72a036395b1bc9fb849f52856b1e1144cd63/fonttools-4.58.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1fac0be6be3e4309058e156948cb73196e5fd994268b89b5e3f5a26ee2b582", size = 5002698, upload-time = "2025-05-28T15:29:01.118Z" }, + { url = "https://files.pythonhosted.org/packages/7b/56/cdddc63333ed77e810df56e5e7fb93659022d535a670335d8792be6d59fd/fonttools-4.58.1-cp313-cp313-win32.whl", hash = "sha256:aed7f93a9a072f0ce6fb46aad9474824ac6dd9c7c38a72f8295dd14f2215950f", size = 2174515, upload-time = "2025-05-28T15:29:03.424Z" }, + { url = "https://files.pythonhosted.org/packages/ba/81/c7f395718e44cebe1010fcd7f1b91957d65d512d5f03114d2d6d00cae1c4/fonttools-4.58.1-cp313-cp313-win_amd64.whl", hash = "sha256:b27d69c97c20c9bca807f7ae7fc7df459eb62994859ff6a2a489e420634deac3", size = 2225290, upload-time = "2025-05-28T15:29:05.099Z" }, { url = "https://files.pythonhosted.org/packages/21/ff/995277586691c0cc314c28b24b4ec30610440fd7bf580072aed1409f95b0/fonttools-4.58.1-py3-none-any.whl", hash = "sha256:db88365d0962cd6f5bce54b190a4669aeed9c9941aa7bd60a5af084d8d9173d6", size = 1113429, upload-time = "2025-05-28T15:29:24.185Z" }, ] @@ -476,7 +459,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/66/910217271189cc3f32f670040235f4bf026ded8ca07270667d69c06e7324/greenlet-3.2.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c49e9f7c6f625507ed83a7485366b46cbe325717c60837f7244fc99ba16ba9d6", size = 267395, upload-time = "2025-05-09T14:50:45.357Z" }, { url = "https://files.pythonhosted.org/packages/a8/36/8d812402ca21017c82880f399309afadb78a0aa300a9b45d741e4df5d954/greenlet-3.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3cc1a3ed00ecfea8932477f729a9f616ad7347a5e55d50929efa50a86cb7be7", size = 625742, upload-time = "2025-05-09T15:23:58.293Z" }, { url = "https://files.pythonhosted.org/packages/7b/77/66d7b59dfb7cc1102b2f880bc61cb165ee8998c9ec13c96606ba37e54c77/greenlet-3.2.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c9896249fbef2c615853b890ee854f22c671560226c9221cfd27c995db97e5c", size = 637014, upload-time = "2025-05-09T15:24:47.025Z" }, - { url = "https://files.pythonhosted.org/packages/36/a7/ff0d408f8086a0d9a5aac47fa1b33a040a9fca89bd5a3f7b54d1cd6e2793/greenlet-3.2.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7409796591d879425997a518138889d8d17e63ada7c99edc0d7a1c22007d4907", size = 632874, upload-time = "2025-05-09T15:29:20.014Z" }, { url = "https://files.pythonhosted.org/packages/a1/75/1dc2603bf8184da9ebe69200849c53c3c1dca5b3a3d44d9f5ca06a930550/greenlet-3.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7791dcb496ec53d60c7f1c78eaa156c21f402dda38542a00afc3e20cae0f480f", size = 631652, upload-time = "2025-05-09T14:53:30.961Z" }, { url = "https://files.pythonhosted.org/packages/7b/74/ddc8c3bd4c2c20548e5bf2b1d2e312a717d44e2eca3eadcfc207b5f5ad80/greenlet-3.2.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d8009ae46259e31bc73dc183e402f548e980c96f33a6ef58cc2e7865db012e13", size = 580619, upload-time = "2025-05-09T14:53:42.049Z" }, { url = "https://files.pythonhosted.org/packages/7e/f2/40f26d7b3077b1c7ae7318a4de1f8ffc1d8ccbad8f1d8979bf5080250fd6/greenlet-3.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fd9fb7c941280e2c837b603850efc93c999ae58aae2b40765ed682a6907ebbc5", size = 1109809, upload-time = "2025-05-09T15:26:59.063Z" }, @@ -485,7 +467,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a3/9f/a47e19261747b562ce88219e5ed8c859d42c6e01e73da6fbfa3f08a7be13/greenlet-3.2.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:dcb9cebbf3f62cb1e5afacae90761ccce0effb3adaa32339a0670fe7805d8068", size = 268635, upload-time = "2025-05-09T14:50:39.007Z" }, { url = "https://files.pythonhosted.org/packages/11/80/a0042b91b66975f82a914d515e81c1944a3023f2ce1ed7a9b22e10b46919/greenlet-3.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf3fc9145141250907730886b031681dfcc0de1c158f3cc51c092223c0f381ce", size = 628786, upload-time = "2025-05-09T15:24:00.692Z" }, { url = "https://files.pythonhosted.org/packages/38/a2/8336bf1e691013f72a6ebab55da04db81a11f68e82bb691f434909fa1327/greenlet-3.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:efcdfb9df109e8a3b475c016f60438fcd4be68cd13a365d42b35914cdab4bb2b", size = 640866, upload-time = "2025-05-09T15:24:48.153Z" }, - { url = "https://files.pythonhosted.org/packages/f8/7e/f2a3a13e424670a5d08826dab7468fa5e403e0fbe0b5f951ff1bc4425b45/greenlet-3.2.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bd139e4943547ce3a56ef4b8b1b9479f9e40bb47e72cc906f0f66b9d0d5cab3", size = 636752, upload-time = "2025-05-09T15:29:23.182Z" }, { url = "https://files.pythonhosted.org/packages/fd/5d/ce4a03a36d956dcc29b761283f084eb4a3863401c7cb505f113f73af8774/greenlet-3.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71566302219b17ca354eb274dfd29b8da3c268e41b646f330e324e3967546a74", size = 636028, upload-time = "2025-05-09T14:53:32.854Z" }, { url = "https://files.pythonhosted.org/packages/4b/29/b130946b57e3ceb039238413790dd3793c5e7b8e14a54968de1fe449a7cf/greenlet-3.2.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3091bc45e6b0c73f225374fefa1536cd91b1e987377b12ef5b19129b07d93ebe", size = 583869, upload-time = "2025-05-09T14:53:43.614Z" }, { url = "https://files.pythonhosted.org/packages/ac/30/9f538dfe7f87b90ecc75e589d20cbd71635531a617a336c386d775725a8b/greenlet-3.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:44671c29da26539a5f142257eaba5110f71887c24d40df3ac87f1117df589e0e", size = 1112886, upload-time = "2025-05-09T15:27:01.304Z" }, @@ -494,22 +475,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/a1/88fdc6ce0df6ad361a30ed78d24c86ea32acb2b563f33e39e927b1da9ea0/greenlet-3.2.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:df4d1509efd4977e6a844ac96d8be0b9e5aa5d5c77aa27ca9f4d3f92d3fcf330", size = 270413, upload-time = "2025-05-09T14:51:32.455Z" }, { url = "https://files.pythonhosted.org/packages/a6/2e/6c1caffd65490c68cd9bcec8cb7feb8ac7b27d38ba1fea121fdc1f2331dc/greenlet-3.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da956d534a6d1b9841f95ad0f18ace637668f680b1339ca4dcfb2c1837880a0b", size = 637242, upload-time = "2025-05-09T15:24:02.63Z" }, { url = "https://files.pythonhosted.org/packages/98/28/088af2cedf8823b6b7ab029a5626302af4ca1037cf8b998bed3a8d3cb9e2/greenlet-3.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c7b15fb9b88d9ee07e076f5a683027bc3befd5bb5d25954bb633c385d8b737e", size = 651444, upload-time = "2025-05-09T15:24:49.856Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9f/0116ab876bb0bc7a81eadc21c3f02cd6100dcd25a1cf2a085a130a63a26a/greenlet-3.2.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:752f0e79785e11180ebd2e726c8a88109ded3e2301d40abced2543aa5d164275", size = 646067, upload-time = "2025-05-09T15:29:24.989Z" }, { url = "https://files.pythonhosted.org/packages/35/17/bb8f9c9580e28a94a9575da847c257953d5eb6e39ca888239183320c1c28/greenlet-3.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ae572c996ae4b5e122331e12bbb971ea49c08cc7c232d1bd43150800a2d6c65", size = 648153, upload-time = "2025-05-09T14:53:34.716Z" }, { url = "https://files.pythonhosted.org/packages/2c/ee/7f31b6f7021b8df6f7203b53b9cc741b939a2591dcc6d899d8042fcf66f2/greenlet-3.2.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02f5972ff02c9cf615357c17ab713737cccfd0eaf69b951084a9fd43f39833d3", size = 603865, upload-time = "2025-05-09T14:53:45.738Z" }, { url = "https://files.pythonhosted.org/packages/b5/2d/759fa59323b521c6f223276a4fc3d3719475dc9ae4c44c2fe7fc750f8de0/greenlet-3.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4fefc7aa68b34b9224490dfda2e70ccf2131368493add64b4ef2d372955c207e", size = 1119575, upload-time = "2025-05-09T15:27:04.248Z" }, { url = "https://files.pythonhosted.org/packages/30/05/356813470060bce0e81c3df63ab8cd1967c1ff6f5189760c1a4734d405ba/greenlet-3.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a31ead8411a027c2c4759113cf2bd473690517494f3d6e4bf67064589afcd3c5", size = 1147460, upload-time = "2025-05-09T14:54:00.315Z" }, { url = "https://files.pythonhosted.org/packages/07/f4/b2a26a309a04fb844c7406a4501331b9400e1dd7dd64d3450472fd47d2e1/greenlet-3.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:b24c7844c0a0afc3ccbeb0b807adeefb7eff2b5599229ecedddcfeb0ef333bec", size = 296239, upload-time = "2025-05-09T14:57:17.633Z" }, - { url = "https://files.pythonhosted.org/packages/37/3a/dbf22e1c7c1affc68ad4bc8f06619945c74a92b112ae6a401bed1f1ed63b/greenlet-3.2.2-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:1e4747712c4365ef6765708f948acc9c10350719ca0545e362c24ab973017370", size = 266190, upload-time = "2025-05-09T14:50:53.356Z" }, - { url = "https://files.pythonhosted.org/packages/33/b1/21fabb65b13f504e8428595c54be73b78e7a542a2bd08ed9e1c56c8fcee2/greenlet-3.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:782743700ab75716650b5238a4759f840bb2dcf7bff56917e9ffdf9f1f23ec59", size = 623904, upload-time = "2025-05-09T15:24:24.588Z" }, - { url = "https://files.pythonhosted.org/packages/ec/9e/3346e463f13b593aafc683df6a85e9495a9b0c16c54c41f7e34353adea40/greenlet-3.2.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:354f67445f5bed6604e493a06a9a49ad65675d3d03477d38a4db4a427e9aad0e", size = 635672, upload-time = "2025-05-09T15:24:53.737Z" }, - { url = "https://files.pythonhosted.org/packages/8e/88/6e8459e4789a276d1a18d656fd95334d21fe0609c6d6f446f88dbfd9483d/greenlet-3.2.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3aeca9848d08ce5eb653cf16e15bb25beeab36e53eb71cc32569f5f3afb2a3aa", size = 630975, upload-time = "2025-05-09T15:29:29.393Z" }, - { url = "https://files.pythonhosted.org/packages/ab/80/81ccf96daf166e8334c37663498dad742d61114cdf801f4872a38e8e31d5/greenlet-3.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cb8553ee954536500d88a1a2f58fcb867e45125e600e80f586ade399b3f8819", size = 630252, upload-time = "2025-05-09T14:53:42.765Z" }, - { url = "https://files.pythonhosted.org/packages/c1/61/3489e3fd3b7dc81c73368177313231a1a1b30df660a0c117830aa18e0f29/greenlet-3.2.2-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1592a615b598643dbfd566bac8467f06c8c8ab6e56f069e573832ed1d5d528cc", size = 579122, upload-time = "2025-05-09T14:53:49.702Z" }, - { url = "https://files.pythonhosted.org/packages/be/55/57685fe335e88f8c75d204f9967e46e5fba601f861fb80821e5fb7ab959d/greenlet-3.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1f72667cc341c95184f1c68f957cb2d4fc31eef81646e8e59358a10ce6689457", size = 1108299, upload-time = "2025-05-09T15:27:10.193Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e2/3f27dd194989e8481ccac3b36932836b596d58f908106b8608f98587d9f7/greenlet-3.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a8fa80665b1a29faf76800173ff5325095f3e66a78e62999929809907aca5659", size = 1132431, upload-time = "2025-05-09T14:54:05.517Z" }, - { url = "https://files.pythonhosted.org/packages/ce/7b/803075f7b1df9165032af07d81d783b04c59e64fb28b09fd7a0e5a249adc/greenlet-3.2.2-cp39-cp39-win32.whl", hash = "sha256:6629311595e3fe7304039c67f00d145cd1d38cf723bb5b99cc987b23c1433d61", size = 277740, upload-time = "2025-05-09T15:13:47.009Z" }, - { url = "https://files.pythonhosted.org/packages/ff/a3/eb7713abfd0a079d24b775d01c6578afbcc6676d89508ab3cbebd5c836ea/greenlet-3.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:eeb27bece45c0c2a5842ac4c5a1b5c2ceaefe5711078eed4e8043159fa05c834", size = 294863, upload-time = "2025-05-09T15:09:46.366Z" }, + { url = "https://files.pythonhosted.org/packages/89/30/97b49779fff8601af20972a62cc4af0c497c1504dfbb3e93be218e093f21/greenlet-3.2.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:3ab7194ee290302ca15449f601036007873028712e92ca15fc76597a0aeb4c59", size = 269150, upload-time = "2025-05-09T14:50:30.784Z" }, + { url = "https://files.pythonhosted.org/packages/21/30/877245def4220f684bc2e01df1c2e782c164e84b32e07373992f14a2d107/greenlet-3.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc5c43bb65ec3669452af0ab10729e8fdc17f87a1f2ad7ec65d4aaaefabf6bf", size = 637381, upload-time = "2025-05-09T15:24:12.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/16/adf937908e1f913856b5371c1d8bdaef5f58f251d714085abeea73ecc471/greenlet-3.2.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:decb0658ec19e5c1f519faa9a160c0fc85a41a7e6654b3ce1b44b939f8bf1325", size = 651427, upload-time = "2025-05-09T15:24:51.074Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e6/28ed5cb929c6b2f001e96b1d0698c622976cd8f1e41fe7ebc047fa7c6dd4/greenlet-3.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1919cbdc1c53ef739c94cf2985056bcc0838c1f217b57647cbf4578576c63825", size = 648398, upload-time = "2025-05-09T14:53:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/9d/70/b200194e25ae86bc57077f695b6cc47ee3118becf54130c5514456cf8dac/greenlet-3.2.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3885f85b61798f4192d544aac7b25a04ece5fe2704670b4ab73c2d2c14ab740d", size = 606795, upload-time = "2025-05-09T14:53:47.039Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c8/ba1def67513a941154ed8f9477ae6e5a03f645be6b507d3930f72ed508d3/greenlet-3.2.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:85f3e248507125bf4af607a26fd6cb8578776197bd4b66e35229cdf5acf1dfbf", size = 1117976, upload-time = "2025-05-09T15:27:06.542Z" }, + { url = "https://files.pythonhosted.org/packages/c3/30/d0e88c1cfcc1b3331d63c2b54a0a3a4a950ef202fb8b92e772ca714a9221/greenlet-3.2.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1e76106b6fc55fa3d6fe1c527f95ee65e324a13b62e243f77b48317346559708", size = 1145509, upload-time = "2025-05-09T14:54:02.223Z" }, + { url = "https://files.pythonhosted.org/packages/90/2e/59d6491834b6e289051b252cf4776d16da51c7c6ca6a87ff97e3a50aa0cd/greenlet-3.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:fe46d4f8e94e637634d54477b0cfabcf93c53f29eedcbdeecaf2af32029b4421", size = 296023, upload-time = "2025-05-09T14:53:24.157Z" }, + { url = "https://files.pythonhosted.org/packages/65/66/8a73aace5a5335a1cba56d0da71b7bd93e450f17d372c5b7c5fa547557e9/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba30e88607fb6990544d84caf3c706c4b48f629e18853fc6a646f82db9629418", size = 629911, upload-time = "2025-05-09T15:24:22.376Z" }, + { url = "https://files.pythonhosted.org/packages/48/08/c8b8ebac4e0c95dcc68ec99198842e7db53eda4ab3fb0a4e785690883991/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:055916fafad3e3388d27dd68517478933a97edc2fc54ae79d3bec827de2c64c4", size = 635251, upload-time = "2025-05-09T15:24:52.205Z" }, + { url = "https://files.pythonhosted.org/packages/10/ec/718a3bd56249e729016b0b69bee4adea0dfccf6ca43d147ef3b21edbca16/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89c69e9a10670eb7a66b8cef6354c24671ba241f46152dd3eed447f79c29fb5b", size = 628851, upload-time = "2025-05-09T14:53:38.472Z" }, + { url = "https://files.pythonhosted.org/packages/9b/9d/d1c79286a76bc62ccdc1387291464af16a4204ea717f24e77b0acd623b99/greenlet-3.2.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02a98600899ca1ca5d3a2590974c9e3ec259503b2d6ba6527605fcd74e08e207", size = 593718, upload-time = "2025-05-09T14:53:48.313Z" }, + { url = "https://files.pythonhosted.org/packages/cd/41/96ba2bf948f67b245784cd294b84e3d17933597dffd3acdb367a210d1949/greenlet-3.2.2-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:b50a8c5c162469c3209e5ec92ee4f95c8231b11db6a04db09bbe338176723bb8", size = 1105752, upload-time = "2025-05-09T15:27:08.217Z" }, + { url = "https://files.pythonhosted.org/packages/68/3b/3b97f9d33c1f2eb081759da62bd6162159db260f602f048bc2f36b4c453e/greenlet-3.2.2-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:45f9f4853fb4cc46783085261c9ec4706628f3b57de3e68bae03e8f8b3c0de51", size = 1125170, upload-time = "2025-05-09T14:54:04.082Z" }, + { url = "https://files.pythonhosted.org/packages/31/df/b7d17d66c8d0f578d2885a3d8f565e9e4725eacc9d3fdc946d0031c055c4/greenlet-3.2.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:9ea5231428af34226c05f927e16fc7f6fa5e39e3ad3cd24ffa48ba53a47f4240", size = 269899, upload-time = "2025-05-09T14:54:01.581Z" }, ] [[package]] @@ -524,6 +509,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/c6/5c20af38c2a57c15d87f7f38bee77d63c1d2a3689f74fefaf35915dd12b2/griffe-1.7.3-py3-none-any.whl", hash = "sha256:c6b3ee30c2f0f17f30bcdef5068d6ab7a2a4f1b8bf1a3e74b56fffd21e1c5f75", size = 129303, upload-time = "2025-04-23T11:29:07.145Z" }, ] +[[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 = "idna" version = "3.10" @@ -542,30 +536,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, ] -[[package]] -name = "importlib-metadata" -version = "8.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, -] - -[[package]] -name = "importlib-resources" -version = "6.5.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, -] - [[package]] name = "iniconfig" version = "2.1.0" @@ -587,102 +557,10 @@ 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 = "kiwisolver" -version = "1.4.7" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286, upload-time = "2024-09-04T09:39:44.302Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/14/fc943dd65268a96347472b4fbe5dcc2f6f55034516f80576cd0dd3a8930f/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6", size = 122440, upload-time = "2024-09-04T09:03:44.9Z" }, - { url = "https://files.pythonhosted.org/packages/1e/46/e68fed66236b69dd02fcdb506218c05ac0e39745d696d22709498896875d/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17", size = 65758, upload-time = "2024-09-04T09:03:46.582Z" }, - { url = "https://files.pythonhosted.org/packages/ef/fa/65de49c85838681fc9cb05de2a68067a683717321e01ddafb5b8024286f0/kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9", size = 64311, upload-time = "2024-09-04T09:03:47.973Z" }, - { url = "https://files.pythonhosted.org/packages/42/9c/cc8d90f6ef550f65443bad5872ffa68f3dee36de4974768628bea7c14979/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9", size = 1637109, upload-time = "2024-09-04T09:03:49.281Z" }, - { url = "https://files.pythonhosted.org/packages/55/91/0a57ce324caf2ff5403edab71c508dd8f648094b18cfbb4c8cc0fde4a6ac/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c", size = 1617814, upload-time = "2024-09-04T09:03:51.444Z" }, - { url = "https://files.pythonhosted.org/packages/12/5d/c36140313f2510e20207708adf36ae4919416d697ee0236b0ddfb6fd1050/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599", size = 1400881, upload-time = "2024-09-04T09:03:53.357Z" }, - { url = "https://files.pythonhosted.org/packages/56/d0/786e524f9ed648324a466ca8df86298780ef2b29c25313d9a4f16992d3cf/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05", size = 1512972, upload-time = "2024-09-04T09:03:55.082Z" }, - { url = "https://files.pythonhosted.org/packages/67/5a/77851f2f201e6141d63c10a0708e996a1363efaf9e1609ad0441b343763b/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407", size = 1444787, upload-time = "2024-09-04T09:03:56.588Z" }, - { url = "https://files.pythonhosted.org/packages/06/5f/1f5eaab84355885e224a6fc8d73089e8713dc7e91c121f00b9a1c58a2195/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278", size = 2199212, upload-time = "2024-09-04T09:03:58.557Z" }, - { url = "https://files.pythonhosted.org/packages/b5/28/9152a3bfe976a0ae21d445415defc9d1cd8614b2910b7614b30b27a47270/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5", size = 2346399, upload-time = "2024-09-04T09:04:00.178Z" }, - { url = "https://files.pythonhosted.org/packages/26/f6/453d1904c52ac3b400f4d5e240ac5fec25263716723e44be65f4d7149d13/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad", size = 2308688, upload-time = "2024-09-04T09:04:02.216Z" }, - { url = "https://files.pythonhosted.org/packages/5a/9a/d4968499441b9ae187e81745e3277a8b4d7c60840a52dc9d535a7909fac3/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895", size = 2445493, upload-time = "2024-09-04T09:04:04.571Z" }, - { url = "https://files.pythonhosted.org/packages/07/c9/032267192e7828520dacb64dfdb1d74f292765f179e467c1cba97687f17d/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3", size = 2262191, upload-time = "2024-09-04T09:04:05.969Z" }, - { url = "https://files.pythonhosted.org/packages/6c/ad/db0aedb638a58b2951da46ddaeecf204be8b4f5454df020d850c7fa8dca8/kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc", size = 46644, upload-time = "2024-09-04T09:04:07.408Z" }, - { url = "https://files.pythonhosted.org/packages/12/ca/d0f7b7ffbb0be1e7c2258b53554efec1fd652921f10d7d85045aff93ab61/kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c", size = 55877, upload-time = "2024-09-04T09:04:08.869Z" }, - { url = "https://files.pythonhosted.org/packages/97/6c/cfcc128672f47a3e3c0d918ecb67830600078b025bfc32d858f2e2d5c6a4/kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a", size = 48347, upload-time = "2024-09-04T09:04:10.106Z" }, - { url = "https://files.pythonhosted.org/packages/e9/44/77429fa0a58f941d6e1c58da9efe08597d2e86bf2b2cce6626834f49d07b/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", size = 122442, upload-time = "2024-09-04T09:04:11.432Z" }, - { url = "https://files.pythonhosted.org/packages/e5/20/8c75caed8f2462d63c7fd65e16c832b8f76cda331ac9e615e914ee80bac9/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", size = 65762, upload-time = "2024-09-04T09:04:12.468Z" }, - { url = "https://files.pythonhosted.org/packages/f4/98/fe010f15dc7230f45bc4cf367b012d651367fd203caaa992fd1f5963560e/kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", size = 64319, upload-time = "2024-09-04T09:04:13.635Z" }, - { url = "https://files.pythonhosted.org/packages/8b/1b/b5d618f4e58c0675654c1e5051bcf42c776703edb21c02b8c74135541f60/kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", size = 1334260, upload-time = "2024-09-04T09:04:14.878Z" }, - { url = "https://files.pythonhosted.org/packages/b8/01/946852b13057a162a8c32c4c8d2e9ed79f0bb5d86569a40c0b5fb103e373/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", size = 1426589, upload-time = "2024-09-04T09:04:16.514Z" }, - { url = "https://files.pythonhosted.org/packages/70/d1/c9f96df26b459e15cf8a965304e6e6f4eb291e0f7a9460b4ad97b047561e/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", size = 1541080, upload-time = "2024-09-04T09:04:18.322Z" }, - { url = "https://files.pythonhosted.org/packages/d3/73/2686990eb8b02d05f3de759d6a23a4ee7d491e659007dd4c075fede4b5d0/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052", size = 1470049, upload-time = "2024-09-04T09:04:20.266Z" }, - { url = "https://files.pythonhosted.org/packages/a7/4b/2db7af3ed3af7c35f388d5f53c28e155cd402a55432d800c543dc6deb731/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", size = 1426376, upload-time = "2024-09-04T09:04:22.419Z" }, - { url = "https://files.pythonhosted.org/packages/05/83/2857317d04ea46dc5d115f0df7e676997bbd968ced8e2bd6f7f19cfc8d7f/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", size = 2222231, upload-time = "2024-09-04T09:04:24.526Z" }, - { url = "https://files.pythonhosted.org/packages/0d/b5/866f86f5897cd4ab6d25d22e403404766a123f138bd6a02ecb2cdde52c18/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", size = 2368634, upload-time = "2024-09-04T09:04:25.899Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ee/73de8385403faba55f782a41260210528fe3273d0cddcf6d51648202d6d0/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", size = 2329024, upload-time = "2024-09-04T09:04:28.523Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e7/cd101d8cd2cdfaa42dc06c433df17c8303d31129c9fdd16c0ea37672af91/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", size = 2468484, upload-time = "2024-09-04T09:04:30.547Z" }, - { url = "https://files.pythonhosted.org/packages/e1/72/84f09d45a10bc57a40bb58b81b99d8f22b58b2040c912b7eb97ebf625bf2/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", size = 2284078, upload-time = "2024-09-04T09:04:33.218Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d4/71828f32b956612dc36efd7be1788980cb1e66bfb3706e6dec9acad9b4f9/kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", size = 46645, upload-time = "2024-09-04T09:04:34.371Z" }, - { url = "https://files.pythonhosted.org/packages/a1/65/d43e9a20aabcf2e798ad1aff6c143ae3a42cf506754bcb6a7ed8259c8425/kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", size = 56022, upload-time = "2024-09-04T09:04:35.786Z" }, - { url = "https://files.pythonhosted.org/packages/35/b3/9f75a2e06f1b4ca00b2b192bc2b739334127d27f1d0625627ff8479302ba/kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", size = 48536, upload-time = "2024-09-04T09:04:37.525Z" }, - { url = "https://files.pythonhosted.org/packages/97/9c/0a11c714cf8b6ef91001c8212c4ef207f772dd84540104952c45c1f0a249/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", size = 121808, upload-time = "2024-09-04T09:04:38.637Z" }, - { url = "https://files.pythonhosted.org/packages/f2/d8/0fe8c5f5d35878ddd135f44f2af0e4e1d379e1c7b0716f97cdcb88d4fd27/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", size = 65531, upload-time = "2024-09-04T09:04:39.694Z" }, - { url = "https://files.pythonhosted.org/packages/80/c5/57fa58276dfdfa612241d640a64ca2f76adc6ffcebdbd135b4ef60095098/kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", size = 63894, upload-time = "2024-09-04T09:04:41.6Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e9/26d3edd4c4ad1c5b891d8747a4f81b1b0aba9fb9721de6600a4adc09773b/kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", size = 1369296, upload-time = "2024-09-04T09:04:42.886Z" }, - { url = "https://files.pythonhosted.org/packages/b6/67/3f4850b5e6cffb75ec40577ddf54f7b82b15269cc5097ff2e968ee32ea7d/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", size = 1461450, upload-time = "2024-09-04T09:04:46.284Z" }, - { url = "https://files.pythonhosted.org/packages/52/be/86cbb9c9a315e98a8dc6b1d23c43cffd91d97d49318854f9c37b0e41cd68/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", size = 1579168, upload-time = "2024-09-04T09:04:47.91Z" }, - { url = "https://files.pythonhosted.org/packages/0f/00/65061acf64bd5fd34c1f4ae53f20b43b0a017a541f242a60b135b9d1e301/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", size = 1507308, upload-time = "2024-09-04T09:04:49.465Z" }, - { url = "https://files.pythonhosted.org/packages/21/e4/c0b6746fd2eb62fe702118b3ca0cb384ce95e1261cfada58ff693aeec08a/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", size = 1464186, upload-time = "2024-09-04T09:04:50.949Z" }, - { url = "https://files.pythonhosted.org/packages/0a/0f/529d0a9fffb4d514f2782c829b0b4b371f7f441d61aa55f1de1c614c4ef3/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", size = 2247877, upload-time = "2024-09-04T09:04:52.388Z" }, - { url = "https://files.pythonhosted.org/packages/d1/e1/66603ad779258843036d45adcbe1af0d1a889a07af4635f8b4ec7dccda35/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", size = 2404204, upload-time = "2024-09-04T09:04:54.385Z" }, - { url = "https://files.pythonhosted.org/packages/8d/61/de5fb1ca7ad1f9ab7970e340a5b833d735df24689047de6ae71ab9d8d0e7/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", size = 2352461, upload-time = "2024-09-04T09:04:56.307Z" }, - { url = "https://files.pythonhosted.org/packages/ba/d2/0edc00a852e369827f7e05fd008275f550353f1f9bcd55db9363d779fc63/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", size = 2501358, upload-time = "2024-09-04T09:04:57.922Z" }, - { url = "https://files.pythonhosted.org/packages/84/15/adc15a483506aec6986c01fb7f237c3aec4d9ed4ac10b756e98a76835933/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", size = 2314119, upload-time = "2024-09-04T09:04:59.332Z" }, - { url = "https://files.pythonhosted.org/packages/36/08/3a5bb2c53c89660863a5aa1ee236912269f2af8762af04a2e11df851d7b2/kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", size = 46367, upload-time = "2024-09-04T09:05:00.804Z" }, - { url = "https://files.pythonhosted.org/packages/19/93/c05f0a6d825c643779fc3c70876bff1ac221f0e31e6f701f0e9578690d70/kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", size = 55884, upload-time = "2024-09-04T09:05:01.924Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f9/3828d8f21b6de4279f0667fb50a9f5215e6fe57d5ec0d61905914f5b6099/kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", size = 48528, upload-time = "2024-09-04T09:05:02.983Z" }, - { url = "https://files.pythonhosted.org/packages/11/88/37ea0ea64512997b13d69772db8dcdc3bfca5442cda3a5e4bb943652ee3e/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd", size = 122449, upload-time = "2024-09-04T09:05:55.311Z" }, - { url = "https://files.pythonhosted.org/packages/4e/45/5a5c46078362cb3882dcacad687c503089263c017ca1241e0483857791eb/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583", size = 65757, upload-time = "2024-09-04T09:05:56.906Z" }, - { url = "https://files.pythonhosted.org/packages/8a/be/a6ae58978772f685d48dd2e84460937761c53c4bbd84e42b0336473d9775/kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417", size = 64312, upload-time = "2024-09-04T09:05:58.384Z" }, - { url = "https://files.pythonhosted.org/packages/f4/04/18ef6f452d311e1e1eb180c9bf5589187fa1f042db877e6fe443ef10099c/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904", size = 1626966, upload-time = "2024-09-04T09:05:59.855Z" }, - { url = "https://files.pythonhosted.org/packages/21/b1/40655f6c3fa11ce740e8a964fa8e4c0479c87d6a7944b95af799c7a55dfe/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a", size = 1607044, upload-time = "2024-09-04T09:06:02.16Z" }, - { url = "https://files.pythonhosted.org/packages/fd/93/af67dbcfb9b3323bbd2c2db1385a7139d8f77630e4a37bb945b57188eb2d/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8", size = 1391879, upload-time = "2024-09-04T09:06:03.908Z" }, - { url = "https://files.pythonhosted.org/packages/40/6f/d60770ef98e77b365d96061d090c0cd9e23418121c55fff188fa4bdf0b54/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2", size = 1504751, upload-time = "2024-09-04T09:06:05.58Z" }, - { url = "https://files.pythonhosted.org/packages/fa/3a/5f38667d313e983c432f3fcd86932177519ed8790c724e07d77d1de0188a/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88", size = 1436990, upload-time = "2024-09-04T09:06:08.126Z" }, - { url = "https://files.pythonhosted.org/packages/cb/3b/1520301a47326e6a6043b502647e42892be33b3f051e9791cc8bb43f1a32/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde", size = 2191122, upload-time = "2024-09-04T09:06:10.345Z" }, - { url = "https://files.pythonhosted.org/packages/cf/c4/eb52da300c166239a2233f1f9c4a1b767dfab98fae27681bfb7ea4873cb6/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c", size = 2338126, upload-time = "2024-09-04T09:06:12.321Z" }, - { url = "https://files.pythonhosted.org/packages/1a/cb/42b92fd5eadd708dd9107c089e817945500685f3437ce1fd387efebc6d6e/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2", size = 2298313, upload-time = "2024-09-04T09:06:14.562Z" }, - { url = "https://files.pythonhosted.org/packages/4f/eb/be25aa791fe5fc75a8b1e0c965e00f942496bc04635c9aae8035f6b76dcd/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb", size = 2437784, upload-time = "2024-09-04T09:06:16.767Z" }, - { url = "https://files.pythonhosted.org/packages/c5/22/30a66be7f3368d76ff95689e1c2e28d382383952964ab15330a15d8bfd03/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327", size = 2253988, upload-time = "2024-09-04T09:06:18.705Z" }, - { url = "https://files.pythonhosted.org/packages/35/d3/5f2ecb94b5211c8a04f218a76133cc8d6d153b0f9cd0b45fad79907f0689/kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644", size = 46980, upload-time = "2024-09-04T09:06:20.106Z" }, - { url = "https://files.pythonhosted.org/packages/ef/17/cd10d020578764ea91740204edc6b3236ed8106228a46f568d716b11feb2/kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4", size = 55847, upload-time = "2024-09-04T09:06:21.407Z" }, - { url = "https://files.pythonhosted.org/packages/91/84/32232502020bd78d1d12be7afde15811c64a95ed1f606c10456db4e4c3ac/kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f", size = 48494, upload-time = "2024-09-04T09:06:22.648Z" }, - { url = "https://files.pythonhosted.org/packages/ac/59/741b79775d67ab67ced9bb38552da688c0305c16e7ee24bba7a2be253fb7/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643", size = 59491, upload-time = "2024-09-04T09:06:24.188Z" }, - { url = "https://files.pythonhosted.org/packages/58/cc/fb239294c29a5656e99e3527f7369b174dd9cc7c3ef2dea7cb3c54a8737b/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706", size = 57648, upload-time = "2024-09-04T09:06:25.559Z" }, - { url = "https://files.pythonhosted.org/packages/3b/ef/2f009ac1f7aab9f81efb2d837301d255279d618d27b6015780115ac64bdd/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6", size = 84257, upload-time = "2024-09-04T09:06:27.038Z" }, - { url = "https://files.pythonhosted.org/packages/81/e1/c64f50987f85b68b1c52b464bb5bf73e71570c0f7782d626d1eb283ad620/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2", size = 80906, upload-time = "2024-09-04T09:06:28.48Z" }, - { url = "https://files.pythonhosted.org/packages/fd/71/1687c5c0a0be2cee39a5c9c389e546f9c6e215e46b691d00d9f646892083/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4", size = 79951, upload-time = "2024-09-04T09:06:29.966Z" }, - { url = "https://files.pythonhosted.org/packages/ea/8b/d7497df4a1cae9367adf21665dd1f896c2a7aeb8769ad77b662c5e2bcce7/kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a", size = 55715, upload-time = "2024-09-04T09:06:31.489Z" }, - { url = "https://files.pythonhosted.org/packages/d5/df/ce37d9b26f07ab90880923c94d12a6ff4d27447096b4c849bfc4339ccfdf/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39", size = 58666, upload-time = "2024-09-04T09:06:43.756Z" }, - { url = "https://files.pythonhosted.org/packages/b0/d3/e4b04f43bc629ac8e186b77b2b1a251cdfa5b7610fa189dc0db622672ce6/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e", size = 57088, upload-time = "2024-09-04T09:06:45.406Z" }, - { url = "https://files.pythonhosted.org/packages/30/1c/752df58e2d339e670a535514d2db4fe8c842ce459776b8080fbe08ebb98e/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608", size = 84321, upload-time = "2024-09-04T09:06:47.557Z" }, - { url = "https://files.pythonhosted.org/packages/f0/f8/fe6484e847bc6e238ec9f9828089fb2c0bb53f2f5f3a79351fde5b565e4f/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674", size = 80776, upload-time = "2024-09-04T09:06:49.235Z" }, - { url = "https://files.pythonhosted.org/packages/9b/57/d7163c0379f250ef763aba85330a19feefb5ce6cb541ade853aaba881524/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225", size = 79984, upload-time = "2024-09-04T09:06:51.336Z" }, - { url = "https://files.pythonhosted.org/packages/8c/95/4a103776c265d13b3d2cd24fb0494d4e04ea435a8ef97e1b2c026d43250b/kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0", size = 55811, upload-time = "2024-09-04T09:06:53.078Z" }, -] - [[package]] name = "kiwisolver" version = "1.4.8" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538, upload-time = "2024-12-24T18:30:51.519Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623, upload-time = "2024-12-24T18:28:17.687Z" }, @@ -730,6 +608,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410, upload-time = "2024-12-24T18:29:39.991Z" }, { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853, upload-time = "2024-12-24T18:29:42.006Z" }, { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424, upload-time = "2024-12-24T18:29:44.38Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156, upload-time = "2024-12-24T18:29:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555, upload-time = "2024-12-24T18:29:46.37Z" }, + { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071, upload-time = "2024-12-24T18:29:47.333Z" }, + { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053, upload-time = "2024-12-24T18:29:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278, upload-time = "2024-12-24T18:29:51.164Z" }, + { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139, upload-time = "2024-12-24T18:29:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517, upload-time = "2024-12-24T18:29:53.941Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952, upload-time = "2024-12-24T18:29:56.523Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132, upload-time = "2024-12-24T18:29:57.989Z" }, + { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997, upload-time = "2024-12-24T18:29:59.393Z" }, + { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060, upload-time = "2024-12-24T18:30:01.338Z" }, + { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471, upload-time = "2024-12-24T18:30:04.574Z" }, + { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793, upload-time = "2024-12-24T18:30:06.25Z" }, + { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855, upload-time = "2024-12-24T18:30:07.535Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430, upload-time = "2024-12-24T18:30:08.504Z" }, + { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294, upload-time = "2024-12-24T18:30:09.508Z" }, + { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736, upload-time = "2024-12-24T18:30:11.039Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194, upload-time = "2024-12-24T18:30:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942, upload-time = "2024-12-24T18:30:18.927Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341, upload-time = "2024-12-24T18:30:22.102Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455, upload-time = "2024-12-24T18:30:24.947Z" }, + { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138, upload-time = "2024-12-24T18:30:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857, upload-time = "2024-12-24T18:30:28.86Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129, upload-time = "2024-12-24T18:30:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538, upload-time = "2024-12-24T18:30:33.334Z" }, + { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661, upload-time = "2024-12-24T18:30:34.939Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710, upload-time = "2024-12-24T18:30:37.281Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213, upload-time = "2024-12-24T18:30:40.019Z" }, { url = "https://files.pythonhosted.org/packages/1f/f9/ae81c47a43e33b93b0a9819cac6723257f5da2a5a60daf46aa5c7226ea85/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", size = 60403, upload-time = "2024-12-24T18:30:41.372Z" }, { url = "https://files.pythonhosted.org/packages/58/ca/f92b5cb6f4ce0c1ebfcfe3e2e42b96917e16f7090e45b21102941924f18f/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", size = 58657, upload-time = "2024-12-24T18:30:42.392Z" }, { url = "https://files.pythonhosted.org/packages/80/28/ae0240f732f0484d3a4dc885d055653c47144bdf59b670aae0ec3c65a7c8/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", size = 84948, upload-time = "2024-12-24T18:30:44.703Z" }, @@ -743,7 +649,8 @@ name = "libcst" version = "1.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyyaml" }, + { name = "pyyaml", marker = "python_full_version < '3.13'" }, + { name = "pyyaml-ft", marker = "python_full_version >= '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1a/7f/33559a16f5e98e8643a463ed5b4d09ecb0589da8ac61b1fcb761809ab037/libcst-1.8.0.tar.gz", hash = "sha256:21cd41dd9bc7ee16f81a6ecf9dc6c044cdaf6af670b85b4754204a5a0c9890d8", size = 778687, upload-time = "2025-05-27T14:23:52.354Z" } wheels = [ @@ -777,16 +684,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/71/da2a1a42b1412231e0925e0aa9be6194399748303108d191c74b86247329/libcst-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6462ec79e043ced913c8ce2329d57b110de9f283c7e11387c112f0614e4e6c1f", size = 2386491, upload-time = "2025-05-27T14:22:41.503Z" }, { url = "https://files.pythonhosted.org/packages/0c/ea/53b89d124b43b768f4cd618dd00c19047b6e322e1bb9cc435beeeb20f4d1/libcst-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:58452ff8a2c448b230154e3b96d1e23497960a42861b4f406383ce6a98ca671e", size = 2095575, upload-time = "2025-05-27T14:22:43.084Z" }, { url = "https://files.pythonhosted.org/packages/13/d8/cfeea7d17cb5c32fcf70dc98be47df587956a5f971a83924e340438bfd64/libcst-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:e7a88d23b4f35dcfe3564f03df512a50b50f8f0805cc020a975a6b5b00c9683a", size = 1982560, upload-time = "2025-05-27T14:22:44.629Z" }, - { url = "https://files.pythonhosted.org/packages/a4/96/2f5a3498386f079764a4ee37787b129464ebf2b77ae83a0363a7f3c14e7f/libcst-1.8.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ba1c01ae2439c3577c01beafeed76a431e13aeb6d920b505100f551e32410313", size = 2192882, upload-time = "2025-05-27T14:23:33.804Z" }, - { url = "https://files.pythonhosted.org/packages/d0/eb/932e1477b4563a7f46d6030b976461a56c5d7e36ffadd60b2afae6a37a0b/libcst-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:69b09f93e8632d3584b6702a0d9f1d20315f24db2cc798a3d894c037768e52c3", size = 2077631, upload-time = "2025-05-27T14:23:35.898Z" }, - { url = "https://files.pythonhosted.org/packages/60/4a/a2f6f6d62efbd344e43847d79919b18c3e8f2db311b29a0b4374445995d2/libcst-1.8.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ce84140e3c728c0c6f74fa402eba1fd90fb1dc1918d71feab43ad251c72fee83", size = 2217475, upload-time = "2025-05-27T14:23:37.864Z" }, - { url = "https://files.pythonhosted.org/packages/b1/cc/d38b7b3baa461fb2473c194294e13b0f618c3513a1df8fcf8163667e565d/libcst-1.8.0-cp39-cp39-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:046acd4f5c2ee8aa14bfb72f76585cfb36ac3e54512ee59c43cd5c704c5c61bf", size = 2188830, upload-time = "2025-05-27T14:23:39.817Z" }, - { url = "https://files.pythonhosted.org/packages/24/49/a58e887f51764114d2202b89ec7dcbc00f3aa787315d949dd695443e16ea/libcst-1.8.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:c603bacb69df93dc7435f2dba9efdadc7e53a87b15104af8b63dc61a8e08e612", size = 2310225, upload-time = "2025-05-27T14:23:41.436Z" }, - { url = "https://files.pythonhosted.org/packages/f8/b1/13cb381b11c91a7a9ba0d78781eb6148144c7fbe51061b61ad65b7b93595/libcst-1.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18ce5f20f24b3aadefdd9e59035f962726247501aa17bc6db6b38741dd3a23db", size = 2400292, upload-time = "2025-05-27T14:23:43.856Z" }, - { url = "https://files.pythonhosted.org/packages/ac/fa/687f2b9c0ab53eea0131f37e73a392680f7ec32aa49d5f3fcd50ee442936/libcst-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:55e0c390bc2a87de27d997ce102fa44303b86eff4ca1789a1a46d8a014cc901a", size = 2278163, upload-time = "2025-05-27T14:23:45.44Z" }, - { url = "https://files.pythonhosted.org/packages/ae/be/cf00ae8ad0bf27b86a65f15438e00cec4c4fc81077fe841ccfe11a5336f9/libcst-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2001d9996851873da21da3fa4d961247db43da3c02bae533d3689437364cd5c2", size = 2386812, upload-time = "2025-05-27T14:23:47.504Z" }, - { url = "https://files.pythonhosted.org/packages/14/92/0f82fe79f698569fcc2e72f0bb6a6669e79802423a7ff71aac47f7ef3a51/libcst-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:264db7653515b7bed35be5ec6bfed3993abfdf96d3db635abbe14d1085ada504", size = 2093586, upload-time = "2025-05-27T14:23:49.158Z" }, - { url = "https://files.pythonhosted.org/packages/98/b4/991cda8ca2331a4972a25f7d5dffe0eae22a594da98f6b141c201351c48e/libcst-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:3dede96093e5e6c999bb767d0211caedb7e2f485fa4226f9d980c0dae07ee02e", size = 1983679, upload-time = "2025-05-27T14:23:50.741Z" }, + { url = "https://files.pythonhosted.org/packages/e1/28/8a488241fa40306b081b20ca4783000dea3f91a1c4e2b4e4d707ab9839e1/libcst-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4e01ed9d0fcc20093fb39ee4252e70d6a993d26871319a8edad4183c8feb55eb", size = 2183878, upload-time = "2025-05-27T14:22:46.203Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ef/4c1033df67546f7e6a380fb91d6ab8dfa93fa33ff1ce5c09dd587bbea461/libcst-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f227625f7be41bd08063d758da33dde338e1460bc790e26b0f2d1475b5f78908", size = 2068087, upload-time = "2025-05-27T14:22:47.695Z" }, + { url = "https://files.pythonhosted.org/packages/97/c7/9e3992956a1bc7ba42a5b8a6b3588f1ae72b9bade74d2ea232644646e77e/libcst-1.8.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:d855ecbea9ae3efbf7e9a851918120196c7195471d06150773b9b789d95e8aa6", size = 2218294, upload-time = "2025-05-27T14:22:49.59Z" }, + { url = "https://files.pythonhosted.org/packages/56/b7/ab142dbb5de5d6023527e11997f9c1583351230b96928b3b5cbf7c912655/libcst-1.8.0-cp313-cp313-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4555f3a357d6f1f99fa09328eed74c0bd4e9fc3a77456acc8a19f1ed087dd69c", size = 2190312, upload-time = "2025-05-27T14:22:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/1a/5b/efbe2312619a3c240d2515c740aad05a08f8e13f386107d664808e9c0a17/libcst-1.8.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ddd4477c13b0014793cdb0d0c686c33ffe1d2c704923c65f4d9f575ce4262a4c", size = 2309025, upload-time = "2025-05-27T14:22:52.929Z" }, + { url = "https://files.pythonhosted.org/packages/9a/97/25391720d0e4f38637cd98b1722c0f673f795680d4975a84bf0154fe4b1a/libcst-1.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e512c00274110df3ce724a637e5f41e5fe90341db8cc9ea03cce35df7933010", size = 2400661, upload-time = "2025-05-27T14:22:54.545Z" }, + { url = "https://files.pythonhosted.org/packages/64/a5/efd688fe117b9c997338c542ed6c1ffab433351eb42c0179e43c1f60956c/libcst-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05d05612b95d064a08b3ecb8e67c4f8d289f1c12a7e3beb8d7b18f007a7c76eb", size = 2279371, upload-time = "2025-05-27T14:22:56.367Z" }, + { url = "https://files.pythonhosted.org/packages/08/13/0b2572183d5488fe7f892a2db60e9504ebc80af39e22a2ca58f830cc93c0/libcst-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bd8328ae3a86fc7001019815dcf84425565e21dd09ecc70bd94f10d287d17034", size = 2386388, upload-time = "2025-05-27T14:22:58.534Z" }, + { url = "https://files.pythonhosted.org/packages/44/ec/8dabe56809cbf60afc3ee832391388c9baf1fcf14f4e4094d4337d12e196/libcst-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7f9b545d607b581f2b3d307f8d5e9524c9e6364535ff4460faefd56395484420", size = 2095604, upload-time = "2025-05-27T14:23:01.075Z" }, + { url = "https://files.pythonhosted.org/packages/11/b5/d5fb9ddf46ceafe6decffb29c9d1bd8f6b8bd4f9b09e263db7e8b113d76a/libcst-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:e0654aab4eb61ee04d72cff9da68e5e7f2fa8c870efbc0d48754970572c0facf", size = 1982489, upload-time = "2025-05-27T14:23:02.77Z" }, + { url = "https://files.pythonhosted.org/packages/30/1e/27420a92a564ea198eb16a0fa7130ee688b456f8fed3a2240eac304537c7/libcst-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e2fa6fbd7d755e59c58745d997f9805bc11c18e0d6042f6f35498fd5ba90a038", size = 2173926, upload-time = "2025-05-27T14:23:04.88Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b9/792b80b1a85b97976af1863e4e252624af493acaf8da95fe492d6c5e1d6f/libcst-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ecd71d54648dee629171272cb264c42eaf5321566747bc264984208abc528a6", size = 2059803, upload-time = "2025-05-27T14:23:07.645Z" }, + { url = "https://files.pythonhosted.org/packages/fb/b6/9c82b49916fa816bdcbf5b3e63f9f65ed318e2ea2cf76f9f05bf563b0017/libcst-1.8.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:f0cbde11baa9fb91799432066d0444a90439479e960da3078f841e1ac0c51dfe", size = 2206555, upload-time = "2025-05-27T14:23:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/b7/31/39c110eb66d5fd7cc4891cf55192a358a6be8b8f6ac0e2eb709850104456/libcst-1.8.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:edfc5636e015b5ef8f8ed8b9628d15eaaf415d906cd4cc6a5fa63cbfdd38a23c", size = 2177856, upload-time = "2025-05-27T14:23:20.91Z" }, + { url = "https://files.pythonhosted.org/packages/7e/66/560cf088ae5b93aea3e35aa3fb3fb2fbc2d5bf4ef0097220027f31488f95/libcst-1.8.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:10e494659e510b5428d2102151149636ad7a6b691bbb57ec7cd7e256938746a9", size = 2299368, upload-time = "2025-05-27T14:23:22.44Z" }, + { url = "https://files.pythonhosted.org/packages/e3/4d/7af5aac7ba3ec04faa40c4fcb2eba16f6259123376fac4eb131ab1af880f/libcst-1.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ae851e0987cc355b8f1dcc9915b4cf8e408dcb6fbb589d47d3db098e67497cd", size = 2376624, upload-time = "2025-05-27T14:23:24.501Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0e/cac4685b0802c2dbd7f88bf100a4b3db92fbdf5bd3f22bc7a7b58139369a/libcst-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:459d8c9ceb2b97058f0ec0775050ec2024ee1a178b421510e7fc12e628b5d2d3", size = 2268894, upload-time = "2025-05-27T14:23:26.577Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b2/6efd6b0d9e88768c7c29ac08d91091a36801d29bbd9deecb16f6e6be7971/libcst-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6913649f1129a7d9a53d22ca602bf1f3c3f4d7cb7d99441dafd0f1bc98601b97", size = 2378827, upload-time = "2025-05-27T14:23:28.767Z" }, + { url = "https://files.pythonhosted.org/packages/99/f8/7d61985de5cb8e65a80c740ee2fa30cd582a91fc2bb860a732e5dccc1276/libcst-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c36850b3df46eedbd9593db28074ae443888d4f22cb71224276b405a7c99d3a", size = 2084719, upload-time = "2025-05-27T14:23:30.664Z" }, + { url = "https://files.pythonhosted.org/packages/81/d8/fb61859af9a838ffa611ea34a855c7133a3018faf877f4d4555019302d0c/libcst-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3741c5d07b3438b6db94b395e1855c4c1d6a3ecd7ef35bfe599aadfa098578a3", size = 1971986, upload-time = "2025-05-27T14:23:32.225Z" }, ] [[package]] @@ -803,12 +720,21 @@ wheels = [ ] [[package]] -name = "markdown" -version = "3.8" +name = "mako" +version = "1.3.11" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { 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 = "markdown" +version = "3.8" +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2f/15/222b423b0b88689c266d9eac4e61396fe2cc53464459d6a37618ac863b24/markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f", size = 360906, upload-time = "2025-04-11T14:42:50.928Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/51/3f/afe76f8e2246ffbc867440cbcf90525264df0e658f8a5ca1f872b3f6192a/markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc", size = 106210, upload-time = "2025-04-11T14:42:49.178Z" }, @@ -850,88 +776,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, - { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344, upload-time = "2024-10-18T15:21:43.721Z" }, - { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389, upload-time = "2024-10-18T15:21:44.666Z" }, - { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607, upload-time = "2024-10-18T15:21:45.452Z" }, - { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728, upload-time = "2024-10-18T15:21:46.295Z" }, - { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826, upload-time = "2024-10-18T15:21:47.134Z" }, - { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843, upload-time = "2024-10-18T15:21:48.334Z" }, - { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219, upload-time = "2024-10-18T15:21:49.587Z" }, - { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946, upload-time = "2024-10-18T15:21:50.441Z" }, - { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063, upload-time = "2024-10-18T15:21:51.385Z" }, - { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" }, -] - -[[package]] -name = "matplotlib" -version = "3.9.4" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "contourpy", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "cycler", marker = "python_full_version < '3.10'" }, - { name = "fonttools", marker = "python_full_version < '3.10'" }, - { name = "importlib-resources", marker = "python_full_version < '3.10'" }, - { name = "kiwisolver", version = "1.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, - { name = "pillow", marker = "python_full_version < '3.10'" }, - { name = "pyparsing", marker = "python_full_version < '3.10'" }, - { name = "python-dateutil", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/17/1747b4154034befd0ed33b52538f5eb7752d05bb51c5e2a31470c3bc7d52/matplotlib-3.9.4.tar.gz", hash = "sha256:1e00e8be7393cbdc6fedfa8a6fba02cf3e83814b285db1c60b906a023ba41bc3", size = 36106529, upload-time = "2024-12-13T05:56:34.184Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/94/27d2e2c30d54b56c7b764acc1874a909e34d1965a427fc7092bb6a588b63/matplotlib-3.9.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fdd7abfb706dfa8d307af64a87f1a862879ec3cd8d0ec8637458f0885b9c50", size = 7885089, upload-time = "2024-12-13T05:54:24.224Z" }, - { url = "https://files.pythonhosted.org/packages/c6/25/828273307e40a68eb8e9df832b6b2aaad075864fdc1de4b1b81e40b09e48/matplotlib-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d89bc4e85e40a71d1477780366c27fb7c6494d293e1617788986f74e2a03d7ff", size = 7770600, upload-time = "2024-12-13T05:54:27.214Z" }, - { url = "https://files.pythonhosted.org/packages/f2/65/f841a422ec994da5123368d76b126acf4fc02ea7459b6e37c4891b555b83/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddf9f3c26aae695c5daafbf6b94e4c1a30d6cd617ba594bbbded3b33a1fcfa26", size = 8200138, upload-time = "2024-12-13T05:54:29.497Z" }, - { url = "https://files.pythonhosted.org/packages/07/06/272aca07a38804d93b6050813de41ca7ab0e29ba7a9dd098e12037c919a9/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18ebcf248030173b59a868fda1fe42397253f6698995b55e81e1f57431d85e50", size = 8312711, upload-time = "2024-12-13T05:54:34.396Z" }, - { url = "https://files.pythonhosted.org/packages/98/37/f13e23b233c526b7e27ad61be0a771894a079e0f7494a10d8d81557e0e9a/matplotlib-3.9.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974896ec43c672ec23f3f8c648981e8bc880ee163146e0312a9b8def2fac66f5", size = 9090622, upload-time = "2024-12-13T05:54:36.808Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8c/b1f5bd2bd70e60f93b1b54c4d5ba7a992312021d0ddddf572f9a1a6d9348/matplotlib-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:4598c394ae9711cec135639374e70871fa36b56afae17bdf032a345be552a88d", size = 7828211, upload-time = "2024-12-13T05:54:40.596Z" }, - { url = "https://files.pythonhosted.org/packages/74/4b/65be7959a8fa118a3929b49a842de5b78bb55475236fcf64f3e308ff74a0/matplotlib-3.9.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4dd29641d9fb8bc4492420c5480398dd40a09afd73aebe4eb9d0071a05fbe0c", size = 7894430, upload-time = "2024-12-13T05:54:44.049Z" }, - { url = "https://files.pythonhosted.org/packages/e9/18/80f70d91896e0a517b4a051c3fd540daa131630fd75e02e250365353b253/matplotlib-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30e5b22e8bcfb95442bf7d48b0d7f3bdf4a450cbf68986ea45fca3d11ae9d099", size = 7780045, upload-time = "2024-12-13T05:54:46.414Z" }, - { url = "https://files.pythonhosted.org/packages/a2/73/ccb381026e3238c5c25c3609ba4157b2d1a617ec98d65a8b4ee4e1e74d02/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bb0030d1d447fd56dcc23b4c64a26e44e898f0416276cac1ebc25522e0ac249", size = 8209906, upload-time = "2024-12-13T05:54:49.459Z" }, - { url = "https://files.pythonhosted.org/packages/ab/33/1648da77b74741c89f5ea95cbf42a291b4b364f2660b316318811404ed97/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aca90ed222ac3565d2752b83dbb27627480d27662671e4d39da72e97f657a423", size = 8322873, upload-time = "2024-12-13T05:54:53.066Z" }, - { url = "https://files.pythonhosted.org/packages/57/d3/8447ba78bc6593c9044c372d1609f8ea10fb1e071e7a9e0747bea74fc16c/matplotlib-3.9.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a181b2aa2906c608fcae72f977a4a2d76e385578939891b91c2550c39ecf361e", size = 9099566, upload-time = "2024-12-13T05:54:55.522Z" }, - { url = "https://files.pythonhosted.org/packages/23/e1/4f0e237bf349c02ff9d1b6e7109f1a17f745263809b9714a8576dc17752b/matplotlib-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:1f6882828231eca17f501c4dcd98a05abb3f03d157fbc0769c6911fe08b6cfd3", size = 7838065, upload-time = "2024-12-13T05:54:58.337Z" }, - { url = "https://files.pythonhosted.org/packages/1a/2b/c918bf6c19d6445d1cefe3d2e42cb740fb997e14ab19d4daeb6a7ab8a157/matplotlib-3.9.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dfc48d67e6661378a21c2983200a654b72b5c5cdbd5d2cf6e5e1ece860f0cc70", size = 7891131, upload-time = "2024-12-13T05:55:02.837Z" }, - { url = "https://files.pythonhosted.org/packages/c1/e5/b4e8fc601ca302afeeabf45f30e706a445c7979a180e3a978b78b2b681a4/matplotlib-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47aef0fab8332d02d68e786eba8113ffd6f862182ea2999379dec9e237b7e483", size = 7776365, upload-time = "2024-12-13T05:55:05.158Z" }, - { url = "https://files.pythonhosted.org/packages/99/06/b991886c506506476e5d83625c5970c656a491b9f80161458fed94597808/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fba1f52c6b7dc764097f52fd9ab627b90db452c9feb653a59945de16752e965f", size = 8200707, upload-time = "2024-12-13T05:55:09.48Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e2/556b627498cb27e61026f2d1ba86a78ad1b836fef0996bef5440e8bc9559/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:173ac3748acaac21afcc3fa1633924609ba1b87749006bc25051c52c422a5d00", size = 8313761, upload-time = "2024-12-13T05:55:12.95Z" }, - { url = "https://files.pythonhosted.org/packages/58/ff/165af33ec766ff818306ea88e91f9f60d2a6ed543be1eb122a98acbf3b0d/matplotlib-3.9.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320edea0cadc07007765e33f878b13b3738ffa9745c5f707705692df70ffe0e0", size = 9095284, upload-time = "2024-12-13T05:55:16.199Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8b/3d0c7a002db3b1ed702731c2a9a06d78d035f1f2fb0fb936a8e43cc1e9f4/matplotlib-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a4a4cfc82330b27042a7169533da7991e8789d180dd5b3daeaee57d75cd5a03b", size = 7841160, upload-time = "2024-12-13T05:55:19.991Z" }, - { url = "https://files.pythonhosted.org/packages/56/eb/501b465c9fef28f158e414ea3a417913dc2ac748564c7ed41535f23445b4/matplotlib-3.9.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3c3724d89a387ddf78ff88d2a30ca78ac2b4c89cf37f2db4bd453c34799e933c", size = 7885919, upload-time = "2024-12-13T05:55:59.66Z" }, - { url = "https://files.pythonhosted.org/packages/da/36/236fbd868b6c91309a5206bd90c3f881f4f44b2d997cd1d6239ef652f878/matplotlib-3.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d5f0a8430ffe23d7e32cfd86445864ccad141797f7d25b7c41759a5b5d17cfd7", size = 7771486, upload-time = "2024-12-13T05:56:04.264Z" }, - { url = "https://files.pythonhosted.org/packages/e0/4b/105caf2d54d5ed11d9f4335398f5103001a03515f2126c936a752ccf1461/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bb0141a21aef3b64b633dc4d16cbd5fc538b727e4958be82a0e1c92a234160e", size = 8201838, upload-time = "2024-12-13T05:56:06.792Z" }, - { url = "https://files.pythonhosted.org/packages/5d/a7/bb01188fb4013d34d274caf44a2f8091255b0497438e8b6c0a7c1710c692/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57aa235109e9eed52e2c2949db17da185383fa71083c00c6c143a60e07e0888c", size = 8314492, upload-time = "2024-12-13T05:56:09.964Z" }, - { url = "https://files.pythonhosted.org/packages/33/19/02e1a37f7141fc605b193e927d0a9cdf9dc124a20b9e68793f4ffea19695/matplotlib-3.9.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b18c600061477ccfdd1e6fd050c33d8be82431700f3452b297a56d9ed7037abb", size = 9092500, upload-time = "2024-12-13T05:56:13.55Z" }, - { url = "https://files.pythonhosted.org/packages/57/68/c2feb4667adbf882ffa4b3e0ac9967f848980d9f8b5bebd86644aa67ce6a/matplotlib-3.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:ef5f2d1b67d2d2145ff75e10f8c008bfbf71d45137c4b648c87193e7dd053eac", size = 7822962, upload-time = "2024-12-13T05:56:16.358Z" }, - { url = "https://files.pythonhosted.org/packages/0c/22/2ef6a364cd3f565442b0b055e0599744f1e4314ec7326cdaaa48a4d864d7/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:44e0ed786d769d85bc787b0606a53f2d8d2d1d3c8a2608237365e9121c1a338c", size = 7877995, upload-time = "2024-12-13T05:56:18.805Z" }, - { url = "https://files.pythonhosted.org/packages/87/b8/2737456e566e9f4d94ae76b8aa0d953d9acb847714f9a7ad80184474f5be/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:09debb9ce941eb23ecdbe7eab972b1c3e0276dcf01688073faff7b0f61d6c6ca", size = 7769300, upload-time = "2024-12-13T05:56:21.315Z" }, - { url = "https://files.pythonhosted.org/packages/b2/1f/e709c6ec7b5321e6568769baa288c7178e60a93a9da9e682b39450da0e29/matplotlib-3.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc53cf157a657bfd03afab14774d54ba73aa84d42cfe2480c91bd94873952db", size = 8313423, upload-time = "2024-12-13T05:56:26.719Z" }, - { url = "https://files.pythonhosted.org/packages/5e/b6/5a1f868782cd13f053a679984e222007ecff654a9bfbac6b27a65f4eeb05/matplotlib-3.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ad45da51be7ad02387801fd154ef74d942f49fe3fcd26a64c94842ba7ec0d865", size = 7854624, upload-time = "2024-12-13T05:56:29.359Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, ] [[package]] name = "matplotlib" version = "3.10.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", -] dependencies = [ - { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "cycler", marker = "python_full_version >= '3.10'" }, - { name = "fonttools", marker = "python_full_version >= '3.10'" }, - { name = "kiwisolver", version = "1.4.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "numpy", marker = "python_full_version >= '3.10'" }, - { name = "packaging", marker = "python_full_version >= '3.10'" }, - { name = "pillow", marker = "python_full_version >= '3.10'" }, - { name = "pyparsing", marker = "python_full_version >= '3.10'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, + { 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/26/91/d49359a21893183ed2a5b6c76bec40e0b1dcbf8ca148f864d134897cfc75/matplotlib-3.10.3.tar.gz", hash = "sha256:2f82d2c5bb7ae93aaaa4cd42aca65d76ce6376f83304fa3a630b569aca274df0", size = 34799811, upload-time = "2025-05-08T19:10:54.39Z" } wheels = [ @@ -953,6 +833,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/91/ba0ae1ff4b3f30972ad01cd4a8029e70a0ec3b8ea5be04764b128b66f763/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed70453fd99733293ace1aec568255bc51c6361cb0da94fa5ebf0649fdb2150a", size = 8601321, upload-time = "2025-05-08T19:10:14.47Z" }, { url = "https://files.pythonhosted.org/packages/d2/88/d636041eb54a84b889e11872d91f7cbf036b3b0e194a70fa064eb8b04f7a/matplotlib-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbed9917b44070e55640bd13419de83b4c918e52d97561544814ba463811cbc7", size = 9406972, upload-time = "2025-05-08T19:10:16.569Z" }, { url = "https://files.pythonhosted.org/packages/b1/79/0d1c165eac44405a86478082e225fce87874f7198300bbebc55faaf6d28d/matplotlib-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:cf37d8c6ef1a48829443e8ba5227b44236d7fcaf7647caa3178a4ff9f7a5be05", size = 8067954, upload-time = "2025-05-08T19:10:18.663Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c1/23cfb566a74c696a3b338d8955c549900d18fe2b898b6e94d682ca21e7c2/matplotlib-3.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9f2efccc8dcf2b86fc4ee849eea5dcaecedd0773b30f47980dc0cbeabf26ec84", size = 8180318, upload-time = "2025-05-08T19:10:20.426Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/02f1c3b66b30da9ee343c343acbb6251bef5b01d34fad732446eaadcd108/matplotlib-3.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ddbba06a6c126e3301c3d272a99dcbe7f6c24c14024e80307ff03791a5f294e", size = 8051132, upload-time = "2025-05-08T19:10:22.569Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ab/8db1a5ac9b3a7352fb914133001dae889f9fcecb3146541be46bed41339c/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748302b33ae9326995b238f606e9ed840bf5886ebafcb233775d946aa8107a15", size = 8457633, upload-time = "2025-05-08T19:10:24.749Z" }, + { url = "https://files.pythonhosted.org/packages/f5/64/41c4367bcaecbc03ef0d2a3ecee58a7065d0a36ae1aa817fe573a2da66d4/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a80fcccbef63302c0efd78042ea3c2436104c5b1a4d3ae20f864593696364ac7", size = 8601031, upload-time = "2025-05-08T19:10:27.03Z" }, + { url = "https://files.pythonhosted.org/packages/12/6f/6cc79e9e5ab89d13ed64da28898e40fe5b105a9ab9c98f83abd24e46d7d7/matplotlib-3.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55e46cbfe1f8586adb34f7587c3e4f7dedc59d5226719faf6cb54fc24f2fd52d", size = 9406988, upload-time = "2025-05-08T19:10:29.056Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0f/eed564407bd4d935ffabf561ed31099ed609e19287409a27b6d336848653/matplotlib-3.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:151d89cb8d33cb23345cd12490c76fd5d18a56581a16d950b48c6ff19bb2ab93", size = 8068034, upload-time = "2025-05-08T19:10:31.221Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e5/2f14791ff69b12b09e9975e1d116d9578ac684460860ce542c2588cb7a1c/matplotlib-3.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c26dd9834e74d164d06433dc7be5d75a1e9890b926b3e57e74fa446e1a62c3e2", size = 8218223, upload-time = "2025-05-08T19:10:33.114Z" }, + { url = "https://files.pythonhosted.org/packages/5c/08/30a94afd828b6e02d0a52cae4a29d6e9ccfcf4c8b56cc28b021d3588873e/matplotlib-3.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:24853dad5b8c84c8c2390fc31ce4858b6df504156893292ce8092d190ef8151d", size = 8094985, upload-time = "2025-05-08T19:10:35.337Z" }, + { url = "https://files.pythonhosted.org/packages/89/44/f3bc6b53066c889d7a1a3ea8094c13af6a667c5ca6220ec60ecceec2dabe/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68f7878214d369d7d4215e2a9075fef743be38fa401d32e6020bab2dfabaa566", size = 8483109, upload-time = "2025-05-08T19:10:37.611Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c7/473bc559beec08ebee9f86ca77a844b65747e1a6c2691e8c92e40b9f42a8/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6929fc618cb6db9cb75086f73b3219bbb25920cb24cee2ea7a12b04971a4158", size = 8618082, upload-time = "2025-05-08T19:10:39.892Z" }, + { url = "https://files.pythonhosted.org/packages/d8/e9/6ce8edd264c8819e37bbed8172e0ccdc7107fe86999b76ab5752276357a4/matplotlib-3.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c7818292a5cc372a2dc4c795e5c356942eb8350b98ef913f7fda51fe175ac5d", size = 9413699, upload-time = "2025-05-08T19:10:42.376Z" }, + { url = "https://files.pythonhosted.org/packages/1b/92/9a45c91089c3cf690b5badd4be81e392ff086ccca8a1d4e3a08463d8a966/matplotlib-3.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4f23ffe95c5667ef8a2b56eea9b53db7f43910fa4a2d5472ae0f72b64deab4d5", size = 8139044, upload-time = "2025-05-08T19:10:44.551Z" }, { url = "https://files.pythonhosted.org/packages/3d/d1/f54d43e95384b312ffa4a74a4326c722f3b8187aaaa12e9a84cdf3037131/matplotlib-3.10.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:86ab63d66bbc83fdb6733471d3bff40897c1e9921cba112accd748eee4bce5e4", size = 8162896, upload-time = "2025-05-08T19:10:46.432Z" }, { url = "https://files.pythonhosted.org/packages/24/a4/fbfc00c2346177c95b353dcf9b5a004106abe8730a62cb6f27e79df0a698/matplotlib-3.10.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a48f9c08bf7444b5d2391a83e75edb464ccda3c380384b36532a0962593a1751", size = 8039702, upload-time = "2025-05-08T19:10:49.634Z" }, { url = "https://files.pythonhosted.org/packages/6a/b9/59e120d24a2ec5fc2d30646adb2efb4621aab3c6d83d66fb2a7a182db032/matplotlib-3.10.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb73d8aa75a237457988f9765e4dfe1c0d2453c5ca4eabc897d4309672c8e014", size = 8594298, upload-time = "2025-05-08T19:10:51.738Z" }, @@ -967,16 +859,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, ] +[[package]] +name = "mirakuru" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "psutil", marker = "sys_platform != 'cygwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/23/db9034ba28c7d89a540ffb8ca789f70dc12079108ece1cd1762295d5c807/mirakuru-3.0.2.tar.gz", hash = "sha256:21192186a8680ea7567ca68170261df3785768b12962dd19fe8cccab15ad3441", size = 29338, upload-time = "2026-02-11T19:41:15.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/5f/a3f1a7f1f6e55de9285b03ae7e0d3c2a15e044b6e3f9b53bef5609ca05f2/mirakuru-3.0.2-py3-none-any.whl", hash = "sha256:10e5dac4a8f26872c63e9cdfdc01b775aaa2beb3ced98abc497279d2dc525b8f", size = 27583, upload-time = "2026-02-11T19:41:13.578Z" }, +] + [[package]] name = "mkdocs" version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "ghp-import" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2" }, { name = "markdown" }, { name = "markupsafe" }, @@ -1012,8 +914,7 @@ name = "mkdocs-click" version = "0.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click" }, { name = "markdown" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a1/c7/8c25f3a3b379def41e6d0bb5c4beeab7aa8a394b17e749f498504102cfa5/mkdocs_click-0.9.0.tar.gz", hash = "sha256:6050917628d4740517541422b607404d044117bc31b770c4f9e9e1939a50c908", size = 18720, upload-time = "2025-04-07T16:59:36.387Z" } @@ -1038,7 +939,6 @@ name = "mkdocs-get-deps" version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "mergedeep" }, { name = "platformdirs" }, { name = "pyyaml" }, @@ -1084,7 +984,6 @@ name = "mkdocstrings" version = "0.29.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2" }, { name = "markdown" }, { name = "markupsafe" }, @@ -1117,6 +1016,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/c4/ffa32f2c7cdb1728026c7a34aab87796b895767893aaa54611a79b4eef45/mkdocstrings_python-1.16.11-py3-none-any.whl", hash = "sha256:25d96cc9c1f9c272ea1bd8222c900b5f852bf46c984003e9c7c56eaa4696190f", size = 124282, upload-time = "2025-05-24T10:41:30.008Z" }, ] +[[package]] +name = "ntplib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/14/6b018fb602602d9f6cc7485cbad7c1be3a85d25cea18c233854f05284aed/ntplib-0.4.0.tar.gz", hash = "sha256:899d8fb5f8c2555213aea95efca02934c7343df6ace9d7628a5176b176906267", size = 7135, upload-time = "2021-05-28T19:08:54.394Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/8c/41da70f6feaca807357206a376b6de2001b439c7f78f53473a914a6dbd1e/ntplib-0.4.0-py2.py3-none-any.whl", hash = "sha256:8d27375329ed7ff38755f7b6d4658b28edc147cadf40338a63a0da8133469d60", size = 6849, upload-time = "2021-05-28T19:08:53.323Z" }, +] + [[package]] name = "numpy" version = "1.26.4" @@ -1147,17 +1055,6 @@ wheels = [ { 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" }, - { url = "https://files.pythonhosted.org/packages/7d/24/ce71dc08f06534269f66e73c04f5709ee024a1afe92a7b6e1d73f158e1f8/numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c", size = 20636301, upload-time = "2024-02-05T23:59:10.976Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8c/ab03a7c25741f9ebc92684a20125fbc9fc1b8e1e700beb9197d750fdff88/numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be", size = 13971216, upload-time = "2024-02-05T23:59:35.472Z" }, - { url = "https://files.pythonhosted.org/packages/6d/64/c3bcdf822269421d85fe0d64ba972003f9bb4aa9a419da64b86856c9961f/numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764", size = 14226281, upload-time = "2024-02-05T23:59:59.372Z" }, - { url = "https://files.pythonhosted.org/packages/54/30/c2a907b9443cf42b90c17ad10c1e8fa801975f01cb9764f3f8eb8aea638b/numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3", size = 18249516, upload-time = "2024-02-06T00:00:32.79Z" }, - { url = "https://files.pythonhosted.org/packages/43/12/01a563fc44c07095996d0129b8899daf89e4742146f7044cdbdb3101c57f/numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd", size = 13882132, upload-time = "2024-02-06T00:00:58.197Z" }, - { url = "https://files.pythonhosted.org/packages/16/ee/9df80b06680aaa23fc6c31211387e0db349e0e36d6a63ba3bd78c5acdf11/numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c", size = 18084181, upload-time = "2024-02-06T00:01:31.21Z" }, - { url = "https://files.pythonhosted.org/packages/28/7d/4b92e2fe20b214ffca36107f1a3e75ef4c488430e64de2d9af5db3a4637d/numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6", size = 5976360, upload-time = "2024-02-06T00:01:43.013Z" }, - { url = "https://files.pythonhosted.org/packages/b5/42/054082bd8220bbf6f297f982f0a8f5479fcbc55c8b511d928df07b965869/numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea", size = 15814633, upload-time = "2024-02-06T00:02:16.694Z" }, - { url = "https://files.pythonhosted.org/packages/3f/72/3df6c1c06fc83d9cfe381cccb4be2532bbd38bf93fbc9fad087b6687f1c0/numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30", size = 20455961, upload-time = "2024-02-06T00:03:05.993Z" }, - { url = "https://files.pythonhosted.org/packages/8e/02/570545bac308b58ffb21adda0f4e220ba716fb658a63c151daecc3293350/numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c", size = 18061071, upload-time = "2024-02-06T00:03:41.5Z" }, - { url = "https://files.pythonhosted.org/packages/f4/5f/fafd8c51235f60d49f7a88e2275e13971e90555b67da52dd6416caec32fe/numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0", size = 15709730, upload-time = "2024-02-06T00:04:11.719Z" }, ] [[package]] @@ -1165,8 +1062,7 @@ name = "numpydoc" version = "1.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "tabulate" }, { name = "tomli", marker = "python_full_version < '3.11'" }, @@ -1178,13 +1074,12 @@ wheels = [ [[package]] name = "opensampl" -version = "1.1.0" +version = "1.1.5" source = { editable = "." } dependencies = [ { name = "allantools" }, { name = "astor" }, - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click" }, { name = "geoalchemy2" }, { name = "jinja2" }, { name = "libcst" }, @@ -1192,6 +1087,7 @@ dependencies = [ { name = "numpy" }, { name = "pandas" }, { name = "psycopg2-binary" }, + { name = "pydanclick" }, { name = "pydantic" }, { name = "pydantic-settings" }, { name = "python-dotenv" }, @@ -1200,13 +1096,23 @@ dependencies = [ { name = "pyyaml" }, { name = "requests" }, { name = "sqlalchemy" }, + { name = "tabulate" }, { name = "tqdm" }, ] [package.optional-dependencies] +backend = [ + { name = "fastapi" }, + { name = "prometheus-client" }, + { name = "uvicorn" }, +] collect = [ + { name = "ntplib" }, { name = "telnetlib3" }, ] +migrations = [ + { name = "alembic" }, +] [package.dev-dependencies] dev = [ @@ -1215,37 +1121,46 @@ dev = [ { name = "mkdocs-gen-files" }, { name = "mkdocs-material" }, { name = "mkdocstrings", extra = ["python"] }, + { name = "psycopg", extra = ["binary"] }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-mock" }, + { name = "pytest-postgresql" }, { name = "ruff" }, { name = "ty" }, ] [package.metadata] requires-dist = [ + { name = "alembic", marker = "extra == 'migrations'" }, { name = "allantools" }, { name = "astor" }, { name = "click", specifier = ">=8.0.0,<9" }, + { name = "fastapi", marker = "extra == 'backend'" }, { name = "geoalchemy2", specifier = "==0.16.0" }, { name = "jinja2", specifier = ">=3.1.6" }, { name = "libcst" }, { name = "loguru", specifier = ">=0.7.0,<0.8" }, + { name = "ntplib", marker = "extra == 'collect'", specifier = ">=0.4.0,<0.5" }, { name = "numpy", specifier = ">=1.26.4,<2" }, { name = "pandas", specifier = ">=2.2.1,<3" }, + { name = "prometheus-client", marker = "extra == 'backend'" }, { name = "psycopg2-binary", specifier = ">=2.9.0,<3" }, + { name = "pydanclick" }, { name = "pydantic", specifier = ">=2.10.3,<3" }, { name = "pydantic-settings", specifier = ">=2.9.0" }, { name = "python-dotenv" }, - { name = "python-multipart", specifier = ">=0.0.20,<0.0.21" }, + { name = "python-multipart", specifier = ">=0.0.26,<0.0.27" }, { name = "pytz", specifier = "~=2024.1" }, { name = "pyyaml", specifier = ">=6.0.0,<7" }, { name = "requests", specifier = ">=2.31.0,<3" }, { name = "sqlalchemy", specifier = ">=2.0.39,<3" }, + { name = "tabulate" }, { name = "telnetlib3", marker = "extra == 'collect'", specifier = "==2.0.4" }, { name = "tqdm", specifier = ">=4.66.2,<5" }, + { name = "uvicorn", marker = "extra == 'backend'" }, ] -provides-extras = ["server", "collect"] +provides-extras = ["migrations", "backend", "collect"] [package.metadata.requires-dev] dev = [ @@ -1254,9 +1169,11 @@ dev = [ { name = "mkdocs-gen-files" }, { name = "mkdocs-material" }, { name = "mkdocstrings", extras = ["python"] }, + { name = "psycopg", extras = ["binary"] }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-mock" }, + { name = "pytest-postgresql" }, { name = "ruff" }, { name = "ty" }, ] @@ -1312,13 +1229,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235, upload-time = "2024-09-20T19:02:07.094Z" }, { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756, upload-time = "2024-09-20T13:09:20.474Z" }, { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248, upload-time = "2024-09-20T13:09:23.137Z" }, - { url = "https://files.pythonhosted.org/packages/ca/8c/8848a4c9b8fdf5a534fe2077af948bf53cd713d77ffbcd7bd15710348fd7/pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39", size = 12595535, upload-time = "2024-09-20T13:09:51.339Z" }, - { url = "https://files.pythonhosted.org/packages/9c/b9/5cead4f63b6d31bdefeb21a679bc5a7f4aaf262ca7e07e2bc1c341b68470/pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30", size = 11319822, upload-time = "2024-09-20T13:09:54.31Z" }, - { url = "https://files.pythonhosted.org/packages/31/af/89e35619fb573366fa68dc26dad6ad2c08c17b8004aad6d98f1a31ce4bb3/pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c", size = 15625439, upload-time = "2024-09-20T19:02:23.689Z" }, - { url = "https://files.pythonhosted.org/packages/3d/dd/bed19c2974296661493d7acc4407b1d2db4e2a482197df100f8f965b6225/pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c", size = 13068928, upload-time = "2024-09-20T13:09:56.746Z" }, - { url = "https://files.pythonhosted.org/packages/31/a3/18508e10a31ea108d746c848b5a05c0711e0278fa0d6f1c52a8ec52b80a5/pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea", size = 16783266, upload-time = "2024-09-20T19:02:26.247Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a5/3429bd13d82bebc78f4d78c3945efedef63a7cd0c15c17b2eeb838d1121f/pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761", size = 14450871, upload-time = "2024-09-20T13:09:59.779Z" }, - { url = "https://files.pythonhosted.org/packages/2f/49/5c30646e96c684570925b772eac4eb0a8cb0ca590fa978f56c5d3ae73ea1/pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e", size = 11618011, upload-time = "2024-09-20T13:10:02.351Z" }, + { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643, upload-time = "2024-09-20T13:09:25.522Z" }, + { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573, upload-time = "2024-09-20T13:09:28.012Z" }, + { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085, upload-time = "2024-09-20T19:02:10.451Z" }, + { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809, upload-time = "2024-09-20T13:09:30.814Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316, upload-time = "2024-09-20T19:02:13.825Z" }, + { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055, upload-time = "2024-09-20T13:09:33.462Z" }, + { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175, upload-time = "2024-09-20T13:09:35.871Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650, upload-time = "2024-09-20T13:09:38.685Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177, upload-time = "2024-09-20T13:09:41.141Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526, upload-time = "2024-09-20T19:02:16.905Z" }, + { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013, upload-time = "2024-09-20T13:09:44.39Z" }, + { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620, upload-time = "2024-09-20T19:02:20.639Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436, upload-time = "2024-09-20T13:09:48.112Z" }, ] [[package]] @@ -1369,17 +1292,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", size = 2332309, upload-time = "2025-04-12T17:48:17.885Z" }, { url = "https://files.pythonhosted.org/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", size = 2676768, upload-time = "2025-04-12T17:48:19.655Z" }, { url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087, upload-time = "2025-04-12T17:48:21.991Z" }, - { url = "https://files.pythonhosted.org/packages/21/3a/c1835d1c7cf83559e95b4f4ed07ab0bb7acc689712adfce406b3f456e9fd/pillow-11.2.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:7491cf8a79b8eb867d419648fff2f83cb0b3891c8b36da92cc7f1931d46108c8", size = 3198391, upload-time = "2025-04-12T17:49:10.122Z" }, - { url = "https://files.pythonhosted.org/packages/b6/4d/dcb7a9af3fc1e8653267c38ed622605d9d1793349274b3ef7af06457e257/pillow-11.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b02d8f9cb83c52578a0b4beadba92e37d83a4ef11570a8688bbf43f4ca50909", size = 3030573, upload-time = "2025-04-12T17:49:11.938Z" }, - { url = "https://files.pythonhosted.org/packages/9d/29/530ca098c1a1eb31d4e163d317d0e24e6d2ead907991c69ca5b663de1bc5/pillow-11.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928", size = 4398677, upload-time = "2025-04-12T17:49:13.861Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ee/0e5e51db34de1690264e5f30dcd25328c540aa11d50a3bc0b540e2a445b6/pillow-11.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3692b68c87096ac6308296d96354eddd25f98740c9d2ab54e1549d6c8aea9d79", size = 4484986, upload-time = "2025-04-12T17:49:15.948Z" }, - { url = "https://files.pythonhosted.org/packages/93/7d/bc723b41ce3d2c28532c47678ec988974f731b5c6fadd5b3a4fba9015e4f/pillow-11.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:f781dcb0bc9929adc77bad571b8621ecb1e4cdef86e940fe2e5b5ee24fd33b35", size = 4501897, upload-time = "2025-04-12T17:49:17.839Z" }, - { url = "https://files.pythonhosted.org/packages/be/0b/532e31abc7389617ddff12551af625a9b03cd61d2989fa595e43c470ec67/pillow-11.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2b490402c96f907a166615e9a5afacf2519e28295f157ec3a2bb9bd57de638cb", size = 4592618, upload-time = "2025-04-12T17:49:19.7Z" }, - { url = "https://files.pythonhosted.org/packages/4c/f0/21ed6499a6216fef753e2e2254a19d08bff3747108ba042422383f3e9faa/pillow-11.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd6b20b93b3ccc9c1b597999209e4bc5cf2853f9ee66e3fc9a400a78733ffc9a", size = 4570493, upload-time = "2025-04-12T17:49:21.703Z" }, - { url = "https://files.pythonhosted.org/packages/68/de/17004ddb8ab855573fe1127ab0168d11378cdfe4a7ee2a792a70ff2e9ba7/pillow-11.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4b835d89c08a6c2ee7781b8dd0a30209a8012b5f09c0a665b65b0eb3560b6f36", size = 4647748, upload-time = "2025-04-12T17:49:23.579Z" }, - { url = "https://files.pythonhosted.org/packages/c7/23/82ecb486384bb3578115c509d4a00bb52f463ee700a5ca1be53da3c88c19/pillow-11.2.1-cp39-cp39-win32.whl", hash = "sha256:b10428b3416d4f9c61f94b494681280be7686bda15898a3a9e08eb66a6d92d67", size = 2331731, upload-time = "2025-04-12T17:49:25.58Z" }, - { url = "https://files.pythonhosted.org/packages/58/bb/87efd58b3689537a623d44dbb2550ef0bb5ff6a62769707a0fe8b1a7bdeb/pillow-11.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:6ebce70c3f486acf7591a3d73431fa504a4e18a9b97ff27f5f47b7368e4b9dd1", size = 2676346, upload-time = "2025-04-12T17:49:27.342Z" }, - { url = "https://files.pythonhosted.org/packages/80/08/dc268475b22887b816e5dcfae31bce897f524b4646bab130c2142c9b2400/pillow-11.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:c27476257b2fdcd7872d54cfd119b3a9ce4610fb85c8e32b70b42e3680a29a1e", size = 2414623, upload-time = "2025-04-12T17:49:29.139Z" }, + { url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098, upload-time = "2025-04-12T17:48:23.915Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166, upload-time = "2025-04-12T17:48:25.738Z" }, + { url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674, upload-time = "2025-04-12T17:48:27.908Z" }, + { url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005, upload-time = "2025-04-12T17:48:29.888Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707, upload-time = "2025-04-12T17:48:31.874Z" }, + { url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008, upload-time = "2025-04-12T17:48:34.422Z" }, + { url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420, upload-time = "2025-04-12T17:48:37.641Z" }, + { url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655, upload-time = "2025-04-12T17:48:39.652Z" }, + { url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329, upload-time = "2025-04-12T17:48:41.765Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388, upload-time = "2025-04-12T17:48:43.625Z" }, + { url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950, upload-time = "2025-04-12T17:48:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759, upload-time = "2025-04-12T17:48:47.866Z" }, + { url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284, upload-time = "2025-04-12T17:48:50.189Z" }, + { url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826, upload-time = "2025-04-12T17:48:52.346Z" }, + { url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329, upload-time = "2025-04-12T17:48:54.403Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049, upload-time = "2025-04-12T17:48:56.383Z" }, + { url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408, upload-time = "2025-04-12T17:48:58.782Z" }, + { url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863, upload-time = "2025-04-12T17:49:00.709Z" }, + { url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938, upload-time = "2025-04-12T17:49:02.946Z" }, + { url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774, upload-time = "2025-04-12T17:49:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895, upload-time = "2025-04-12T17:49:06.635Z" }, + { url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234, upload-time = "2025-04-12T17:49:08.399Z" }, { url = "https://files.pythonhosted.org/packages/33/49/c8c21e4255b4f4a2c0c68ac18125d7f5460b109acc6dfdef1a24f9b960ef/pillow-11.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156", size = 3181727, upload-time = "2025-04-12T17:49:31.898Z" }, { url = "https://files.pythonhosted.org/packages/6d/f1/f7255c0838f8c1ef6d55b625cfb286835c17e8136ce4351c5577d02c443b/pillow-11.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772", size = 2999833, upload-time = "2025-04-12T17:49:34.2Z" }, { url = "https://files.pythonhosted.org/packages/e2/57/9968114457bd131063da98d87790d080366218f64fa2943b65ac6739abb3/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363", size = 3437472, upload-time = "2025-04-12T17:49:36.294Z" }, @@ -1414,6 +1348,132 @@ 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 = "port-for" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/a0/80a64e8cc096c7a9d0f546a28994af849b4775afc5e4ee44bf2739a55115/port_for-1.0.0.tar.gz", hash = "sha256:404d161b1b2c82e2f6b31d8646396b4847d02bf5ee10068c92b7263657a14582", size = 21681, upload-time = "2025-09-30T10:22:51.149Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/2c/b1faca65b9728b4ac43f0bee4bb9e7294bd0a62cc2ee59fd59403bf575f6/port_for-1.0.0-py3-none-any.whl", hash = "sha256:35a848b98cf4cc075fe80dc49ae5c3a78e3ca345a23bd39bf5252277b4eef5c2", size = 17544, upload-time = "2025-09-30T10:22:49.878Z" }, +] + +[[package]] +name = "prometheus-client" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/fb/d9aa83ffe43ce1f19e557c0971d04b90561b0cfd50762aafb01968285553/prometheus_client-0.25.0.tar.gz", hash = "sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28", size = 86035, upload-time = "2026-04-09T19:53:42.359Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9b/d4b1e644385499c8346fa9b622a3f030dce14cd6ef8a1871c221a17a67e7/prometheus_client-0.25.0-py3-none-any.whl", hash = "sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1", size = 64154, upload-time = "2026-04-09T19:53:41.324Z" }, +] + +[[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 = "psycopg" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/b6/379d0a960f8f435ec78720462fd94c4863e7a31237cf81bf76d0af5883bf/psycopg-3.3.3.tar.gz", hash = "sha256:5e9a47458b3c1583326513b2556a2a9473a1001a56c9efe9e587245b43148dd9", size = 165624, upload-time = "2026-02-18T16:52:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/5b/181e2e3becb7672b502f0ed7f16ed7352aca7c109cfb94cf3878a9186db9/psycopg-3.3.3-py3-none-any.whl", hash = "sha256:f96525a72bcfade6584ab17e89de415ff360748c766f0106959144dcbb38c698", size = 212768, upload-time = "2026-02-18T16:46:27.365Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/d8/a763308a41e2ecfb6256ba0877d340c2f2b124c8b2746401863d96fa2c7a/psycopg_binary-3.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b3385b58b2fe408a13d084c14b8dcf468cd36cbbe774408250facc128f9fa75c", size = 4609758, upload-time = "2026-02-18T16:46:33.132Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a9/f8a683e85400c1208685e7c895abc049dc13aa0b6ea989e6adf0a3681fe0/psycopg_binary-3.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1bef235a50a80f6aba05147002bc354559657cb6386dbd04d8e1c97d1d7cbe84", size = 4676740, upload-time = "2026-02-18T16:46:42.904Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7d/03512c4aaac8a58fc3b1221f38293aa517a1950d10ef8646c72c49addc7d/psycopg_binary-3.3.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:97c839717bf8c8df3f6d983a20949c4fb22e2a34ee172e3e427ede363feda27b", size = 5496335, upload-time = "2026-02-18T16:46:51.517Z" }, + { url = "https://files.pythonhosted.org/packages/8a/bc/23319b4b1c2c0b810d225e1b6f16efbb16150074fc0ea96bfcabdf59ee09/psycopg_binary-3.3.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:48e500cf1c0984dacf1f28ea482c3cdbb4c2288d51c336c04bc64198ab21fc51", size = 5172032, upload-time = "2026-02-18T16:47:00.878Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c8/6d61dc0a56654c558a37b2d9b2094e470aa12621305cc7935fd769122e32/psycopg_binary-3.3.3-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb36a08859b9432d94ea6b26ec41a2f98f83f14868c91321d0c1e11f672eeae7", size = 6763107, upload-time = "2026-02-18T16:47:11.784Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b5/e2a3c90aa1059f5b5f593379caad7be3cc3c2ce1ddfc7730e39854e174fe/psycopg_binary-3.3.3-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dde92cfde09293fb63b3f547919ba7d73bd2654573c03502b3263dd0218e44e", size = 5006494, upload-time = "2026-02-18T16:47:17.062Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3e/bf126e0a1f864e191b7f3eeea667ee2ce13d582b036255fb8b12946d1f7a/psycopg_binary-3.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:78c9ce98caaf82ac8484d269791c1b403d7598633e0e4e2fa1097baae244e2f1", size = 4533850, upload-time = "2026-02-18T16:47:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d8/bb5e8d395deb945629aa0c65d12ab90ec3bfcbdf56be89e2a84d001864c9/psycopg_binary-3.3.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d593612758d0041cb13cb0003f7f8d3fabb7ad9319e651e78afae49b1cf5860e", size = 4223316, upload-time = "2026-02-18T16:47:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/c2/70/33eef61b0f0fd41ebf93b9699f44067313a45016827f67b3c8cc41f0a7ab/psycopg_binary-3.3.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f24e8e17035200a465c178e9ea945527ad0738118694184c450f1192a452ff25", size = 3954515, upload-time = "2026-02-18T16:47:30.434Z" }, + { url = "https://files.pythonhosted.org/packages/ea/db/27c2b3b9698e713e83e11e8540daa27516f9e90390ec21a41091cb15fcaf/psycopg_binary-3.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e7b607f0e14f2a4cf7e78a05ebd13df6144acfba87cb90842e70d3f125d9f53f", size = 4260274, upload-time = "2026-02-18T16:47:36.128Z" }, + { url = "https://files.pythonhosted.org/packages/a1/3b/71e5d603059bf5474215f573a3e2d357a4e95672b26e04d41674400d4862/psycopg_binary-3.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:b27d3a23c79fa59557d2cc63a7e8bb4c7e022c018558eda36f9d7c4e6b99a6e0", size = 3557375, upload-time = "2026-02-18T16:47:42.799Z" }, + { url = "https://files.pythonhosted.org/packages/be/c0/b389119dd754483d316805260f3e73cdcad97925839107cc7a296f6132b1/psycopg_binary-3.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a89bb9ee11177b2995d87186b1d9fa892d8ea725e85eab28c6525e4cc14ee048", size = 4609740, upload-time = "2026-02-18T16:47:51.093Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9976eef20f61840285174d360da4c820a311ab39d6b82fa09fbb545be825/psycopg_binary-3.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f7d0cf072c6fbac3795b08c98ef9ea013f11db609659dcfc6b1f6cc31f9e181", size = 4676837, upload-time = "2026-02-18T16:47:55.523Z" }, + { url = "https://files.pythonhosted.org/packages/9f/f2/d28ba2f7404fd7f68d41e8a11df86313bd646258244cb12a8dd83b868a97/psycopg_binary-3.3.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:90eecd93073922f085967f3ed3a98ba8c325cbbc8c1a204e300282abd2369e13", size = 5497070, upload-time = "2026-02-18T16:47:59.929Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/6c5c54b815edeb30a281cfcea96dc93b3bb6be939aea022f00cab7aa1420/psycopg_binary-3.3.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dac7ee2f88b4d7bb12837989ca354c38d400eeb21bce3b73dac02622f0a3c8d6", size = 5172410, upload-time = "2026-02-18T16:48:05.665Z" }, + { url = "https://files.pythonhosted.org/packages/51/75/8206c7008b57de03c1ada46bd3110cc3743f3fd9ed52031c4601401d766d/psycopg_binary-3.3.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b62cf8784eb6d35beaee1056d54caf94ec6ecf2b7552395e305518ab61eb8fd2", size = 6763408, upload-time = "2026-02-18T16:48:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5a/ea1641a1e6c8c8b3454b0fcb43c3045133a8b703e6e824fae134088e63bd/psycopg_binary-3.3.3-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a39f34c9b18e8f6794cca17bfbcd64572ca2482318db644268049f8c738f35a6", size = 5006255, upload-time = "2026-02-18T16:48:22.176Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fb/538df099bf55ae1637d52d7ccb6b9620b535a40f4c733897ac2b7bb9e14c/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:883d68d48ca9ff3cb3d10c5fdebea02c79b48eecacdddbf7cce6e7cdbdc216b8", size = 4532694, upload-time = "2026-02-18T16:48:27.338Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d1/00780c0e187ea3c13dfc53bd7060654b2232cd30df562aac91a5f1c545ac/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:cab7bc3d288d37a80aa8c0820033250c95e40b1c2b5c57cf59827b19c2a8b69d", size = 4222833, upload-time = "2026-02-18T16:48:31.221Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/a07f1ff713c51d64dc9f19f2c32be80299a2055d5d109d5853662b922cb4/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:56c767007ca959ca32f796b42379fc7e1ae2ed085d29f20b05b3fc394f3715cc", size = 3952818, upload-time = "2026-02-18T16:48:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/d3/67/d33f268a7759b4445f3c9b5a181039b01af8c8263c865c1be7a6444d4749/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:da2f331a01af232259a21573a01338530c6016dcfad74626c01330535bcd8628", size = 4258061, upload-time = "2026-02-18T16:48:41.365Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3b/0d8d2c5e8e29ccc07d28c8af38445d9d9abcd238d590186cac82ee71fc84/psycopg_binary-3.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:19f93235ece6dbfc4036b5e4f6d8b13f0b8f2b3eeb8b0bd2936d406991bcdd40", size = 3558915, upload-time = "2026-02-18T16:48:46.679Z" }, + { url = "https://files.pythonhosted.org/packages/90/15/021be5c0cbc5b7c1ab46e91cc3434eb42569f79a0592e67b8d25e66d844d/psycopg_binary-3.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6698dbab5bcef8fdb570fc9d35fd9ac52041771bfcfe6fd0fc5f5c4e36f1e99d", size = 4591170, upload-time = "2026-02-18T16:48:55.594Z" }, + { url = "https://files.pythonhosted.org/packages/f1/54/a60211c346c9a2f8c6b272b5f2bbe21f6e11800ce7f61e99ba75cf8b63e1/psycopg_binary-3.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:329ff393441e75f10b673ae99ab45276887993d49e65f141da20d915c05aafd8", size = 4670009, upload-time = "2026-02-18T16:49:03.608Z" }, + { url = "https://files.pythonhosted.org/packages/c1/53/ac7c18671347c553362aadbf65f92786eef9540676ca24114cc02f5be405/psycopg_binary-3.3.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:eb072949b8ebf4082ae24289a2b0fd724da9adc8f22743409d6fd718ddb379df", size = 5469735, upload-time = "2026-02-18T16:49:10.128Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c3/4f4e040902b82a344eff1c736cde2f2720f127fe939c7e7565706f96dd44/psycopg_binary-3.3.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:263a24f39f26e19ed7fc982d7859a36f17841b05bebad3eb47bb9cd2dd785351", size = 5152919, upload-time = "2026-02-18T16:49:16.335Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e7/d929679c6a5c212bcf738806c7c89f5b3d0919f2e1685a0e08d6ff877945/psycopg_binary-3.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5152d50798c2fa5bd9b68ec68eb68a1b71b95126c1d70adaa1a08cd5eefdc23d", size = 6738785, upload-time = "2026-02-18T16:49:22.687Z" }, + { url = "https://files.pythonhosted.org/packages/69/b0/09703aeb69a9443d232d7b5318d58742e8ca51ff79f90ffe6b88f1db45e7/psycopg_binary-3.3.3-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d6a1e56dd267848edb824dbeb08cf5bac649e02ee0b03ba883ba3f4f0bd54f2", size = 4979008, upload-time = "2026-02-18T16:49:27.313Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a6/e662558b793c6e13a7473b970fee327d635270e41eded3090ef14045a6a5/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73eaaf4bb04709f545606c1db2f65f4000e8a04cdbf3e00d165a23004692093e", size = 4508255, upload-time = "2026-02-18T16:49:31.575Z" }, + { url = "https://files.pythonhosted.org/packages/5f/7f/0f8b2e1d5e0093921b6f324a948a5c740c1447fbb45e97acaf50241d0f39/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:162e5675efb4704192411eaf8e00d07f7960b679cd3306e7efb120bb8d9456cc", size = 4189166, upload-time = "2026-02-18T16:49:35.801Z" }, + { url = "https://files.pythonhosted.org/packages/92/ec/ce2e91c33bc8d10b00c87e2f6b0fb570641a6a60042d6a9ae35658a3a797/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:fab6b5e37715885c69f5d091f6ff229be71e235f272ebaa35158d5a46fd548a0", size = 3924544, upload-time = "2026-02-18T16:49:41.129Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2f/7718141485f73a924205af60041c392938852aa447a94c8cbd222ff389a1/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a4aab31bd6d1057f287c96c0effca3a25584eb9cc702f282ecb96ded7814e830", size = 4235297, upload-time = "2026-02-18T16:49:46.726Z" }, + { url = "https://files.pythonhosted.org/packages/57/f9/1add717e2643a003bbde31b1b220172e64fbc0cb09f06429820c9173f7fc/psycopg_binary-3.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:59aa31fe11a0e1d1bcc2ce37ed35fe2ac84cd65bb9036d049b1a1c39064d0f14", size = 3547659, upload-time = "2026-02-18T16:49:52.999Z" }, + { url = "https://files.pythonhosted.org/packages/03/0a/cac9fdf1df16a269ba0e5f0f06cac61f826c94cadb39df028cdfe19d3a33/psycopg_binary-3.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05f32239aec25c5fb15f7948cffdc2dc0dac098e48b80a140e4ba32b572a2e7d", size = 4590414, upload-time = "2026-02-18T16:50:01.441Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c0/d8f8508fbf440edbc0099b1abff33003cd80c9e66eb3a1e78834e3fb4fb9/psycopg_binary-3.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c84f9d214f2d1de2fafebc17fa68ac3f6561a59e291553dfc45ad299f4898c1", size = 4669021, upload-time = "2026-02-18T16:50:08.803Z" }, + { url = "https://files.pythonhosted.org/packages/04/05/097016b77e343b4568feddf12c72171fc513acef9a4214d21b9478569068/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e77957d2ba17cada11be09a5066d93026cdb61ada7c8893101d7fe1c6e1f3925", size = 5467453, upload-time = "2026-02-18T16:50:14.985Z" }, + { url = "https://files.pythonhosted.org/packages/91/23/73244e5feb55b5ca109cede6e97f32ef45189f0fdac4c80d75c99862729d/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:42961609ac07c232a427da7c87a468d3c82fee6762c220f38e37cfdacb2b178d", size = 5151135, upload-time = "2026-02-18T16:50:24.82Z" }, + { url = "https://files.pythonhosted.org/packages/11/49/5309473b9803b207682095201d8708bbc7842ddf3f192488a69204e36455/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae07a3114313dd91fce686cab2f4c44af094398519af0e0f854bc707e1aeedf1", size = 6737315, upload-time = "2026-02-18T16:50:35.106Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5d/03abe74ef34d460b33c4d9662bf6ec1dd38888324323c1a1752133c10377/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d257c58d7b36a621dcce1d01476ad8b60f12d80eb1406aee4cf796f88b2ae482", size = 4979783, upload-time = "2026-02-18T16:50:42.067Z" }, + { url = "https://files.pythonhosted.org/packages/f0/6c/3fbf8e604e15f2f3752900434046c00c90bb8764305a1b81112bff30ba24/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:07c7211f9327d522c9c47560cae00a4ecf6687f4e02d779d035dd3177b41cb12", size = 4509023, upload-time = "2026-02-18T16:50:50.116Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6b/1a06b43b7c7af756c80b67eac8bfaa51d77e68635a8a8d246e4f0bb7604a/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8e7e9eca9b363dbedeceeadd8be97149d2499081f3c52d141d7cd1f395a91f83", size = 4185874, upload-time = "2026-02-18T16:50:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d3/bf49e3dcaadba510170c8d111e5e69e5ae3f981c1554c5bb71c75ce354bb/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:cb85b1d5702877c16f28d7b92ba030c1f49ebcc9b87d03d8c10bf45a2f1c7508", size = 3925668, upload-time = "2026-02-18T16:51:03.299Z" }, + { url = "https://files.pythonhosted.org/packages/f8/92/0aac830ed6a944fe334404e1687a074e4215630725753f0e3e9a9a595b62/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d4606c84d04b80f9138d72f1e28c6c02dc5ae0c7b8f3f8aaf89c681ce1cd1b1", size = 4234973, upload-time = "2026-02-18T16:51:09.097Z" }, + { url = "https://files.pythonhosted.org/packages/2e/96/102244653ee5a143ece5afe33f00f52fe64e389dfce8dbc87580c6d70d3d/psycopg_binary-3.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:74eae563166ebf74e8d950ff359be037b85723d99ca83f57d9b244a871d6c13b", size = 3551342, upload-time = "2026-02-18T16:51:13.892Z" }, + { url = "https://files.pythonhosted.org/packages/a2/71/7a57e5b12275fe7e7d84d54113f0226080423a869118419c9106c083a21c/psycopg_binary-3.3.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:497852c5eaf1f0c2d88ab74a64a8097c099deac0c71de1cbcf18659a8a04a4b2", size = 4607368, upload-time = "2026-02-18T16:51:19.295Z" }, + { url = "https://files.pythonhosted.org/packages/c7/04/cb834f120f2b2c10d4003515ef9ca9d688115b9431735e3936ae48549af8/psycopg_binary-3.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:258d1ea53464d29768bf25930f43291949f4c7becc706f6e220c515a63a24edd", size = 4687047, upload-time = "2026-02-18T16:51:23.84Z" }, + { url = "https://files.pythonhosted.org/packages/40/e9/47a69692d3da9704468041aa5ed3ad6fc7f6bb1a5ae788d261a26bbca6c7/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:111c59897a452196116db12e7f608da472fbff000693a21040e35fc978b23430", size = 5487096, upload-time = "2026-02-18T16:51:29.645Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b6/0e0dd6a2f802864a4ae3dbadf4ec620f05e3904c7842b326aafc43e5f464/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:17bb6600e2455993946385249a3c3d0af52cd70c1c1cdbf712e9d696d0b0bf1b", size = 5168720, upload-time = "2026-02-18T16:51:36.499Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0d/977af38ac19a6b55d22dff508bd743fd7c1901e1b73657e7937c7cccb0a3/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:642050398583d61c9856210568eb09a8e4f2fe8224bf3be21b67a370e677eead", size = 6762076, upload-time = "2026-02-18T16:51:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/34/40/912a39d48322cf86895c0eaf2d5b95cb899402443faefd4b09abbba6b6e1/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:533efe6dc3a7cba5e2a84e38970786bb966306863e45f3db152007e9f48638a6", size = 4997623, upload-time = "2026-02-18T16:51:47.707Z" }, + { url = "https://files.pythonhosted.org/packages/98/0c/c14d0e259c65dc7be854d926993f151077887391d5a081118907a9d89603/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5958dbf28b77ce2033482f6cb9ef04d43f5d8f4b7636e6963d5626f000efb23e", size = 4532096, upload-time = "2026-02-18T16:51:51.421Z" }, + { url = "https://files.pythonhosted.org/packages/39/21/8b7c50a194cfca6ea0fd4d1f276158307785775426e90700ab2eba5cd623/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a6af77b6626ce92b5817bf294b4d45ec1a6161dba80fc2d82cdffdd6814fd023", size = 4208884, upload-time = "2026-02-18T16:51:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2c/a4981bf42cf30ebba0424971d7ce70a222ae9b82594c42fc3f2105d7b525/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:47f06fcbe8542b4d96d7392c476a74ada521c5aebdb41c3c0155f6595fc14c8d", size = 3944542, upload-time = "2026-02-18T16:52:04.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/e9/b7c29b56aa0b85a4e0c4d89db691c1ceef08f46a356369144430c155a2f5/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e7800e6c6b5dc4b0ca7cc7370f770f53ac83886b76afda0848065a674231e856", size = 4254339, upload-time = "2026-02-18T16:52:10.444Z" }, + { url = "https://files.pythonhosted.org/packages/98/5a/291d89f44d3820fffb7a04ebc8f3ef5dda4f542f44a5daea0c55a84abf45/psycopg_binary-3.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:165f22ab5a9513a3d7425ffb7fcc7955ed8ccaeef6d37e369d6cc1dff1582383", size = 3652796, upload-time = "2026-02-18T16:52:14.02Z" }, +] + [[package]] name = "psycopg2-binary" version = "2.9.10" @@ -1456,17 +1516,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2d/70/aa69c9f69cf09a01da224909ff6ce8b68faeef476f00f7ec377e8f03be70/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", size = 2959312, upload-time = "2024-10-16T11:21:25.584Z" }, { url = "https://files.pythonhosted.org/packages/d3/bd/213e59854fafe87ba47814bf413ace0dcee33a89c8c8c814faca6bc7cf3c/psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", size = 1025191, upload-time = "2024-10-16T11:21:29.912Z" }, { url = "https://files.pythonhosted.org/packages/92/29/06261ea000e2dc1e22907dbbc483a1093665509ea586b29b8986a0e56733/psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", size = 1164031, upload-time = "2024-10-16T11:21:34.211Z" }, - { url = "https://files.pythonhosted.org/packages/a2/bc/e77648009b6e61af327c607543f65fdf25bcfb4100f5a6f3bdb62ddac03c/psycopg2_binary-2.9.10-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b", size = 3043437, upload-time = "2024-10-16T11:23:42.946Z" }, - { url = "https://files.pythonhosted.org/packages/e0/e8/5a12211a1f5b959f3e3ccd342eace60c1f26422f53e06d687821dc268780/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc", size = 2851340, upload-time = "2024-10-16T11:23:50.038Z" }, - { url = "https://files.pythonhosted.org/packages/47/ed/5932b0458a7fc61237b653df050513c8d18a6f4083cc7f90dcef967f7bce/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697", size = 3080905, upload-time = "2024-10-16T11:23:57.932Z" }, - { url = "https://files.pythonhosted.org/packages/71/df/8047d85c3d23864aca4613c3be1ea0fe61dbe4e050a89ac189f9dce4403e/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481", size = 3264640, upload-time = "2024-10-16T11:24:06.122Z" }, - { url = "https://files.pythonhosted.org/packages/f3/de/6157e4ef242920e8f2749f7708d5cc8815414bdd4a27a91996e7cd5c80df/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648", size = 3019812, upload-time = "2024-10-16T11:24:17.025Z" }, - { url = "https://files.pythonhosted.org/packages/25/f9/0fc49efd2d4d6db3a8d0a3f5749b33a0d3fdd872cad49fbf5bfce1c50027/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d", size = 2871933, upload-time = "2024-10-16T11:24:24.858Z" }, - { url = "https://files.pythonhosted.org/packages/57/bc/2ed1bd182219065692ed458d218d311b0b220b20662d25d913bc4e8d3549/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30", size = 2820990, upload-time = "2024-10-16T11:24:29.571Z" }, - { url = "https://files.pythonhosted.org/packages/71/2a/43f77a9b8ee0b10e2de784d97ddc099d9fe0d9eec462a006e4d2cc74756d/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c", size = 2919352, upload-time = "2024-10-16T11:24:36.906Z" }, - { url = "https://files.pythonhosted.org/packages/57/86/d2943df70469e6afab3b5b8e1367fccc61891f46de436b24ddee6f2c8404/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287", size = 2957614, upload-time = "2024-10-16T11:24:44.423Z" }, - { url = "https://files.pythonhosted.org/packages/85/21/195d69371330983aa16139e60ba855d0a18164c9295f3a3696be41bbcd54/psycopg2_binary-2.9.10-cp39-cp39-win32.whl", hash = "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8", size = 1025341, upload-time = "2024-10-16T11:24:48.056Z" }, - { url = "https://files.pythonhosted.org/packages/ad/53/73196ebc19d6fbfc22427b982fbc98698b7b9c361e5e7707e3a3247cf06d/psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5", size = 1163958, upload-time = "2024-10-16T11:24:51.882Z" }, + { url = "https://files.pythonhosted.org/packages/3e/30/d41d3ba765609c0763505d565c4d12d8f3c79793f0d0f044ff5a28bf395b/psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", size = 3044699, upload-time = "2024-10-16T11:21:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/35/44/257ddadec7ef04536ba71af6bc6a75ec05c5343004a7ec93006bee66c0bc/psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", size = 3275245, upload-time = "2024-10-16T11:21:51.989Z" }, + { url = "https://files.pythonhosted.org/packages/1b/11/48ea1cd11de67f9efd7262085588790a95d9dfcd9b8a687d46caf7305c1a/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", size = 2851631, upload-time = "2024-10-16T11:21:57.584Z" }, + { url = "https://files.pythonhosted.org/packages/62/e0/62ce5ee650e6c86719d621a761fe4bc846ab9eff8c1f12b1ed5741bf1c9b/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", size = 3082140, upload-time = "2024-10-16T11:22:02.005Z" }, + { url = "https://files.pythonhosted.org/packages/27/ce/63f946c098611f7be234c0dd7cb1ad68b0b5744d34f68062bb3c5aa510c8/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", size = 3264762, upload-time = "2024-10-16T11:22:06.412Z" }, + { url = "https://files.pythonhosted.org/packages/43/25/c603cd81402e69edf7daa59b1602bd41eb9859e2824b8c0855d748366ac9/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", size = 3020967, upload-time = "2024-10-16T11:22:11.583Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d6/8708d8c6fca531057fa170cdde8df870e8b6a9b136e82b361c65e42b841e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", size = 2872326, upload-time = "2024-10-16T11:22:16.406Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712, upload-time = "2024-10-16T11:22:21.366Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155, upload-time = "2024-10-16T11:22:25.684Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356, upload-time = "2024-10-16T11:22:30.562Z" }, + { url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224, upload-time = "2025-01-04T20:09:19.234Z" }, +] + +[[package]] +name = "pydanclick" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/b5/4de7da87d181c8809e3be95a61256a9efa557030b097f50bb6ddeefcd126/pydanclick-0.5.1.tar.gz", hash = "sha256:ddbf2ec8ce3086b0e011bd1b7cd13474de7210d27e031a37c9cec4a76971d518", size = 20911, upload-time = "2025-02-26T07:39:21.603Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/45/a7e33af2da5d4396e974f33ed7ed406e2cc218427cf8fdbe01c9491e51f8/pydanclick-0.5.1-py3-none-any.whl", hash = "sha256:9ef7dd384f0e04c9db3908251db8f5fe7c05d52a8824c659778036cc671f416a", size = 22163, upload-time = "2025-02-26T07:39:19.926Z" }, ] [[package]] @@ -1534,19 +1603,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, - { url = "https://files.pythonhosted.org/packages/53/ea/bbe9095cdd771987d13c82d104a9c8559ae9aec1e29f139e286fd2e9256e/pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d", size = 2028677, upload-time = "2025-04-23T18:32:27.227Z" }, - { url = "https://files.pythonhosted.org/packages/49/1d/4ac5ed228078737d457a609013e8f7edc64adc37b91d619ea965758369e5/pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954", size = 1864735, upload-time = "2025-04-23T18:32:29.019Z" }, - { url = "https://files.pythonhosted.org/packages/23/9a/2e70d6388d7cda488ae38f57bc2f7b03ee442fbcf0d75d848304ac7e405b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb", size = 1898467, upload-time = "2025-04-23T18:32:31.119Z" }, - { url = "https://files.pythonhosted.org/packages/ff/2e/1568934feb43370c1ffb78a77f0baaa5a8b6897513e7a91051af707ffdc4/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7", size = 1983041, upload-time = "2025-04-23T18:32:33.655Z" }, - { url = "https://files.pythonhosted.org/packages/01/1a/1a1118f38ab64eac2f6269eb8c120ab915be30e387bb561e3af904b12499/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4", size = 2136503, upload-time = "2025-04-23T18:32:35.519Z" }, - { url = "https://files.pythonhosted.org/packages/5c/da/44754d1d7ae0f22d6d3ce6c6b1486fc07ac2c524ed8f6eca636e2e1ee49b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b", size = 2736079, upload-time = "2025-04-23T18:32:37.659Z" }, - { url = "https://files.pythonhosted.org/packages/4d/98/f43cd89172220ec5aa86654967b22d862146bc4d736b1350b4c41e7c9c03/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3", size = 2006508, upload-time = "2025-04-23T18:32:39.637Z" }, - { url = "https://files.pythonhosted.org/packages/2b/cc/f77e8e242171d2158309f830f7d5d07e0531b756106f36bc18712dc439df/pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a", size = 2113693, upload-time = "2025-04-23T18:32:41.818Z" }, - { url = "https://files.pythonhosted.org/packages/54/7a/7be6a7bd43e0a47c147ba7fbf124fe8aaf1200bc587da925509641113b2d/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782", size = 2074224, upload-time = "2025-04-23T18:32:44.033Z" }, - { url = "https://files.pythonhosted.org/packages/2a/07/31cf8fadffbb03be1cb520850e00a8490c0927ec456e8293cafda0726184/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9", size = 2245403, upload-time = "2025-04-23T18:32:45.836Z" }, - { url = "https://files.pythonhosted.org/packages/b6/8d/bbaf4c6721b668d44f01861f297eb01c9b35f612f6b8e14173cb204e6240/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e", size = 2242331, upload-time = "2025-04-23T18:32:47.618Z" }, - { url = "https://files.pythonhosted.org/packages/bb/93/3cc157026bca8f5006250e74515119fcaa6d6858aceee8f67ab6dc548c16/pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9", size = 1910571, upload-time = "2025-04-23T18:32:49.401Z" }, - { url = "https://files.pythonhosted.org/packages/5b/90/7edc3b2a0d9f0dda8806c04e511a67b0b7a41d2187e2003673a996fb4310/pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3", size = 1956504, upload-time = "2025-04-23T18:32:51.287Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" }, { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" }, { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" }, @@ -1565,15 +1638,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, - { url = "https://files.pythonhosted.org/packages/08/98/dbf3fdfabaf81cda5622154fda78ea9965ac467e3239078e0dcd6df159e7/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101", size = 2024034, upload-time = "2025-04-23T18:33:32.843Z" }, - { url = "https://files.pythonhosted.org/packages/8d/99/7810aa9256e7f2ccd492590f86b79d370df1e9292f1f80b000b6a75bd2fb/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64", size = 1858578, upload-time = "2025-04-23T18:33:34.912Z" }, - { url = "https://files.pythonhosted.org/packages/d8/60/bc06fa9027c7006cc6dd21e48dbf39076dc39d9abbaf718a1604973a9670/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d", size = 1892858, upload-time = "2025-04-23T18:33:36.933Z" }, - { url = "https://files.pythonhosted.org/packages/f2/40/9d03997d9518816c68b4dfccb88969756b9146031b61cd37f781c74c9b6a/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535", size = 2068498, upload-time = "2025-04-23T18:33:38.997Z" }, - { url = "https://files.pythonhosted.org/packages/d8/62/d490198d05d2d86672dc269f52579cad7261ced64c2df213d5c16e0aecb1/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d", size = 2108428, upload-time = "2025-04-23T18:33:41.18Z" }, - { url = "https://files.pythonhosted.org/packages/9a/ec/4cd215534fd10b8549015f12ea650a1a973da20ce46430b68fc3185573e8/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6", size = 2069854, upload-time = "2025-04-23T18:33:43.446Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1a/abbd63d47e1d9b0d632fee6bb15785d0889c8a6e0a6c3b5a8e28ac1ec5d2/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca", size = 2237859, upload-time = "2025-04-23T18:33:45.56Z" }, - { url = "https://files.pythonhosted.org/packages/80/1c/fa883643429908b1c90598fd2642af8839efd1d835b65af1f75fba4d94fe/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039", size = 2239059, upload-time = "2025-04-23T18:33:47.735Z" }, - { url = "https://files.pythonhosted.org/packages/d4/29/3cade8a924a61f60ccfa10842f75eb12787e1440e2b8660ceffeb26685e7/pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27", size = 2066661, upload-time = "2025-04-23T18:33:49.995Z" }, ] [[package]] @@ -1663,6 +1727,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" }, ] +[[package]] +name = "pytest-postgresql" +version = "8.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mirakuru" }, + { name = "packaging" }, + { name = "port-for" }, + { name = "psycopg" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/c8/b9607675904b3e4004c76109741aac6479daecfe6fb38ad89f330c6c5adc/pytest_postgresql-8.0.0.tar.gz", hash = "sha256:26cbd44a0adef76cf4a82a3a2263f0e029bc54b5863556a9bd86ca3edfa91cce", size = 49737, upload-time = "2026-01-23T21:14:34.554Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/cf/2c962ce63904e2b051b644a0036bdeeae8ae05d91e11e829b220c3db9935/pytest_postgresql-8.0.0-py3-none-any.whl", hash = "sha256:125b63b16d630c2dea19807062ed4c96e6123f06058ce65b82b4b14174d6c4b8", size = 40228, upload-time = "2026-01-23T21:14:33.08Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1686,11 +1766,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.20" +version = "0.0.26" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, + { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, ] [[package]] @@ -1735,15 +1815,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, - { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" }, - { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" }, - { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" }, - { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" }, - { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" }, - { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, ] [[package]] @@ -1758,6 +1838,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, ] +[[package]] +name = "pyyaml-ft" +version = "8.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/eb/5a0d575de784f9a1f94e2b1288c6886f13f34185e13117ed530f32b6f8a8/pyyaml_ft-8.0.0.tar.gz", hash = "sha256:0c947dce03954c7b5d38869ed4878b2e6ff1d44b08a0d84dc83fdad205ae39ab", size = 141057, upload-time = "2025-06-10T15:32:15.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/ba/a067369fe61a2e57fb38732562927d5bae088c73cb9bb5438736a9555b29/pyyaml_ft-8.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c1306282bc958bfda31237f900eb52c9bedf9b93a11f82e1aab004c9a5657a6", size = 187027, upload-time = "2025-06-10T15:31:48.722Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c5/a3d2020ce5ccfc6aede0d45bcb870298652ac0cf199f67714d250e0cdf39/pyyaml_ft-8.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30c5f1751625786c19de751e3130fc345ebcba6a86f6bddd6e1285342f4bbb69", size = 176146, upload-time = "2025-06-10T15:31:50.584Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bb/23a9739291086ca0d3189eac7cd92b4d00e9fdc77d722ab610c35f9a82ba/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fa992481155ddda2e303fcc74c79c05eddcdbc907b888d3d9ce3ff3e2adcfb0", size = 746792, upload-time = "2025-06-10T15:31:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c2/e8825f4ff725b7e560d62a3609e31d735318068e1079539ebfde397ea03e/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cec6c92b4207004b62dfad1f0be321c9f04725e0f271c16247d8b39c3bf3ea42", size = 786772, upload-time = "2025-06-10T15:31:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/35/be/58a4dcae8854f2fdca9b28d9495298fd5571a50d8430b1c3033ec95d2d0e/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06237267dbcab70d4c0e9436d8f719f04a51123f0ca2694c00dd4b68c338e40b", size = 778723, upload-time = "2025-06-10T15:31:56.093Z" }, + { url = "https://files.pythonhosted.org/packages/86/ed/fed0da92b5d5d7340a082e3802d84c6dc9d5fa142954404c41a544c1cb92/pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a7f332bc565817644cdb38ffe4739e44c3e18c55793f75dddb87630f03fc254", size = 758478, upload-time = "2025-06-10T15:31:58.314Z" }, + { url = "https://files.pythonhosted.org/packages/f0/69/ac02afe286275980ecb2dcdc0156617389b7e0c0a3fcdedf155c67be2b80/pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d10175a746be65f6feb86224df5d6bc5c049ebf52b89a88cf1cd78af5a367a8", size = 799159, upload-time = "2025-06-10T15:31:59.675Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ac/c492a9da2e39abdff4c3094ec54acac9747743f36428281fb186a03fab76/pyyaml_ft-8.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:58e1015098cf8d8aec82f360789c16283b88ca670fe4275ef6c48c5e30b22a96", size = 158779, upload-time = "2025-06-10T15:32:01.029Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9b/41998df3298960d7c67653669f37710fa2d568a5fc933ea24a6df60acaf6/pyyaml_ft-8.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5f3e2ceb790d50602b2fd4ec37abbd760a8c778e46354df647e7c5a4ebb", size = 191331, upload-time = "2025-06-10T15:32:02.602Z" }, + { url = "https://files.pythonhosted.org/packages/0f/16/2710c252ee04cbd74d9562ebba709e5a284faeb8ada88fcda548c9191b47/pyyaml_ft-8.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d445bf6ea16bb93c37b42fdacfb2f94c8e92a79ba9e12768c96ecde867046d1", size = 182879, upload-time = "2025-06-10T15:32:04.466Z" }, + { url = "https://files.pythonhosted.org/packages/9a/40/ae8163519d937fa7bfa457b6f78439cc6831a7c2b170e4f612f7eda71815/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c56bb46b4fda34cbb92a9446a841da3982cdde6ea13de3fbd80db7eeeab8b49", size = 811277, upload-time = "2025-06-10T15:32:06.214Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/28d82dbff7f87b96f0eeac79b7d972a96b4980c1e445eb6a857ba91eda00/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab0abb46eb1780da486f022dce034b952c8ae40753627b27a626d803926483b", size = 831650, upload-time = "2025-06-10T15:32:08.076Z" }, + { url = "https://files.pythonhosted.org/packages/e8/df/161c4566facac7d75a9e182295c223060373d4116dead9cc53a265de60b9/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd48d639cab5ca50ad957b6dd632c7dd3ac02a1abe0e8196a3c24a52f5db3f7a", size = 815755, upload-time = "2025-06-10T15:32:09.435Z" }, + { url = "https://files.pythonhosted.org/packages/05/10/f42c48fa5153204f42eaa945e8d1fd7c10d6296841dcb2447bf7da1be5c4/pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:052561b89d5b2a8e1289f326d060e794c21fa068aa11255fe71d65baf18a632e", size = 810403, upload-time = "2025-06-10T15:32:11.051Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d2/e369064aa51009eb9245399fd8ad2c562bd0bcd392a00be44b2a824ded7c/pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3bb4b927929b0cb162fb1605392a321e3333e48ce616cdcfa04a839271373255", size = 835581, upload-time = "2025-06-10T15:32:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/c0/28/26534bed77109632a956977f60d8519049f545abc39215d086e33a61f1f2/pyyaml_ft-8.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:de04cfe9439565e32f178106c51dd6ca61afaa2907d143835d501d84703d3793", size = 171579, upload-time = "2025-06-10T15:32:14.34Z" }, +] + [[package]] name = "requests" version = "2.32.3" @@ -1807,55 +1911,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/42/d58086ec20f52d2b0140752ae54b355ea2be2ed46f914231136dd1effcc7/ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5", size = 10697770, upload-time = "2025-05-29T13:31:38.009Z" }, ] -[[package]] -name = "scipy" -version = "1.13.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "numpy", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ae/00/48c2f661e2816ccf2ecd77982f6605b2950afe60f60a52b4cbbc2504aa8f/scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c", size = 57210720, upload-time = "2024-05-23T03:29:26.079Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/59/41b2529908c002ade869623b87eecff3e11e3ce62e996d0bdcb536984187/scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca", size = 39328076, upload-time = "2024-05-23T03:19:01.687Z" }, - { url = "https://files.pythonhosted.org/packages/d5/33/f1307601f492f764062ce7dd471a14750f3360e33cd0f8c614dae208492c/scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f", size = 30306232, upload-time = "2024-05-23T03:19:09.089Z" }, - { url = "https://files.pythonhosted.org/packages/c0/66/9cd4f501dd5ea03e4a4572ecd874936d0da296bd04d1c45ae1a4a75d9c3a/scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989", size = 33743202, upload-time = "2024-05-23T03:19:15.138Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ba/7255e5dc82a65adbe83771c72f384d99c43063648456796436c9a5585ec3/scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f", size = 38577335, upload-time = "2024-05-23T03:19:21.984Z" }, - { url = "https://files.pythonhosted.org/packages/49/a5/bb9ded8326e9f0cdfdc412eeda1054b914dfea952bda2097d174f8832cc0/scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94", size = 38820728, upload-time = "2024-05-23T03:19:28.225Z" }, - { url = "https://files.pythonhosted.org/packages/12/30/df7a8fcc08f9b4a83f5f27cfaaa7d43f9a2d2ad0b6562cced433e5b04e31/scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54", size = 46210588, upload-time = "2024-05-23T03:19:35.661Z" }, - { url = "https://files.pythonhosted.org/packages/b4/15/4a4bb1b15bbd2cd2786c4f46e76b871b28799b67891f23f455323a0cdcfb/scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9", size = 39333805, upload-time = "2024-05-23T03:19:43.081Z" }, - { url = "https://files.pythonhosted.org/packages/ba/92/42476de1af309c27710004f5cdebc27bec62c204db42e05b23a302cb0c9a/scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326", size = 30317687, upload-time = "2024-05-23T03:19:48.799Z" }, - { url = "https://files.pythonhosted.org/packages/80/ba/8be64fe225360a4beb6840f3cbee494c107c0887f33350d0a47d55400b01/scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299", size = 33694638, upload-time = "2024-05-23T03:19:55.104Z" }, - { url = "https://files.pythonhosted.org/packages/36/07/035d22ff9795129c5a847c64cb43c1fa9188826b59344fee28a3ab02e283/scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa", size = 38569931, upload-time = "2024-05-23T03:20:01.82Z" }, - { url = "https://files.pythonhosted.org/packages/d9/10/f9b43de37e5ed91facc0cfff31d45ed0104f359e4f9a68416cbf4e790241/scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59", size = 38838145, upload-time = "2024-05-23T03:20:09.173Z" }, - { url = "https://files.pythonhosted.org/packages/4a/48/4513a1a5623a23e95f94abd675ed91cfb19989c58e9f6f7d03990f6caf3d/scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b", size = 46196227, upload-time = "2024-05-23T03:20:16.433Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7b/fb6b46fbee30fc7051913068758414f2721003a89dd9a707ad49174e3843/scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1", size = 39357301, upload-time = "2024-05-23T03:20:23.538Z" }, - { url = "https://files.pythonhosted.org/packages/dc/5a/2043a3bde1443d94014aaa41e0b50c39d046dda8360abd3b2a1d3f79907d/scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d", size = 30363348, upload-time = "2024-05-23T03:20:29.885Z" }, - { url = "https://files.pythonhosted.org/packages/e7/cb/26e4a47364bbfdb3b7fb3363be6d8a1c543bcd70a7753ab397350f5f189a/scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627", size = 33406062, upload-time = "2024-05-23T03:20:36.012Z" }, - { url = "https://files.pythonhosted.org/packages/88/ab/6ecdc526d509d33814835447bbbeedbebdec7cca46ef495a61b00a35b4bf/scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884", size = 38218311, upload-time = "2024-05-23T03:20:42.086Z" }, - { url = "https://files.pythonhosted.org/packages/0b/00/9f54554f0f8318100a71515122d8f4f503b1a2c4b4cfab3b4b68c0eb08fa/scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16", size = 38442493, upload-time = "2024-05-23T03:20:48.292Z" }, - { url = "https://files.pythonhosted.org/packages/3e/df/963384e90733e08eac978cd103c34df181d1fec424de383cdc443f418dd4/scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949", size = 45910955, upload-time = "2024-05-23T03:20:55.091Z" }, - { url = "https://files.pythonhosted.org/packages/7f/29/c2ea58c9731b9ecb30b6738113a95d147e83922986b34c685b8f6eefde21/scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5", size = 39352927, upload-time = "2024-05-23T03:21:01.95Z" }, - { url = "https://files.pythonhosted.org/packages/5c/c0/e71b94b20ccf9effb38d7147c0064c08c622309fd487b1b677771a97d18c/scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24", size = 30324538, upload-time = "2024-05-23T03:21:07.634Z" }, - { url = "https://files.pythonhosted.org/packages/6d/0f/aaa55b06d474817cea311e7b10aab2ea1fd5d43bc6a2861ccc9caec9f418/scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004", size = 33732190, upload-time = "2024-05-23T03:21:14.41Z" }, - { url = "https://files.pythonhosted.org/packages/35/f5/d0ad1a96f80962ba65e2ce1de6a1e59edecd1f0a7b55990ed208848012e0/scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d", size = 38612244, upload-time = "2024-05-23T03:21:21.827Z" }, - { url = "https://files.pythonhosted.org/packages/8d/02/1165905f14962174e6569076bcc3315809ae1291ed14de6448cc151eedfd/scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c", size = 38845637, upload-time = "2024-05-23T03:21:28.729Z" }, - { url = "https://files.pythonhosted.org/packages/3e/77/dab54fe647a08ee4253963bcd8f9cf17509c8ca64d6335141422fe2e2114/scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2", size = 46227440, upload-time = "2024-05-23T03:21:35.888Z" }, -] - [[package]] name = "scipy" version = "1.15.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version == '3.10.*'", -] dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.10'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -1886,6 +1947,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, ] [[package]] @@ -1906,63 +1985,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, ] -[[package]] -name = "sphinx" -version = "7.4.7" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "babel", marker = "python_full_version < '3.10'" }, - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version < '3.10'" }, - { name = "imagesize", marker = "python_full_version < '3.10'" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, - { name = "jinja2", marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, - { name = "pygments", marker = "python_full_version < '3.10'" }, - { name = "requests", marker = "python_full_version < '3.10'" }, - { name = "snowballstemmer", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.10'" }, - { name = "tomli", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911, upload-time = "2024-07-20T14:46:56.059Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624, upload-time = "2024-07-20T14:46:52.142Z" }, -] - [[package]] name = "sphinx" version = "8.1.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.10.*'", + "python_full_version < '3.11'", ] dependencies = [ - { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "babel", marker = "python_full_version == '3.10.*'" }, - { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version == '3.10.*'" }, - { name = "imagesize", marker = "python_full_version == '3.10.*'" }, - { name = "jinja2", marker = "python_full_version == '3.10.*'" }, - { name = "packaging", marker = "python_full_version == '3.10.*'" }, - { name = "pygments", marker = "python_full_version == '3.10.*'" }, - { name = "requests", marker = "python_full_version == '3.10.*'" }, - { name = "snowballstemmer", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.10.*'" }, - { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "alabaster", marker = "python_full_version < '3.11'" }, + { name = "babel", marker = "python_full_version < '3.11'" }, + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version < '3.11'" }, + { name = "imagesize", marker = "python_full_version < '3.11'" }, + { name = "jinja2", marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "requests", marker = "python_full_version < '3.11'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } wheels = [ @@ -1974,11 +2021,12 @@ name = "sphinx" version = "8.2.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.12'", + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", "python_full_version == '3.11.*'", ] dependencies = [ - { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "alabaster", marker = "python_full_version >= '3.11'" }, { name = "babel", marker = "python_full_version >= '3.11'" }, { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, { name = "docutils", marker = "python_full_version >= '3.11'" }, @@ -2060,7 +2108,7 @@ name = "sqlalchemy" version = "2.0.41" 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 = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload-time = "2025-05-14T17:10:32.339Z" } @@ -2089,17 +2137,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/69/cab11fecc7eb64bc561011be2bd03d065b762d87add52a4ca0aca2e12904/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae", size = 3268074, upload-time = "2025-05-14T17:51:51.736Z" }, { url = "https://files.pythonhosted.org/packages/5c/ca/0c19ec16858585d37767b167fc9602593f98998a68a798450558239fb04a/sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6", size = 2084514, upload-time = "2025-05-14T17:55:49.915Z" }, { url = "https://files.pythonhosted.org/packages/7f/23/4c2833d78ff3010a4e17f984c734f52b531a8c9060a50429c9d4b0211be6/sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0", size = 2111557, upload-time = "2025-05-14T17:55:51.349Z" }, - { url = "https://files.pythonhosted.org/packages/dd/1c/3d2a893c020fcc18463794e0a687de58044d1c8a9892d23548ca7e71274a/sqlalchemy-2.0.41-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9a420a91913092d1e20c86a2f5f1fc85c1a8924dbcaf5e0586df8aceb09c9cc2", size = 2121327, upload-time = "2025-05-14T18:01:30.842Z" }, - { url = "https://files.pythonhosted.org/packages/3e/84/389c8f7c7b465682c4e5ba97f6e7825149a6625c629e09b5e872ec3b378f/sqlalchemy-2.0.41-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:906e6b0d7d452e9a98e5ab8507c0da791856b2380fdee61b765632bb8698026f", size = 2110739, upload-time = "2025-05-14T18:01:32.881Z" }, - { url = "https://files.pythonhosted.org/packages/b2/3d/036e84ecb46d6687fa57dc25ab366dff50773a19364def210b8770fd1516/sqlalchemy-2.0.41-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a373a400f3e9bac95ba2a06372c4fd1412a7cee53c37fc6c05f829bf672b8769", size = 3198018, upload-time = "2025-05-14T17:57:53.791Z" }, - { url = "https://files.pythonhosted.org/packages/8d/de/112e2142bf730a16a6cb43efc87e36dd62426e155727490c041130c6e852/sqlalchemy-2.0.41-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:087b6b52de812741c27231b5a3586384d60c353fbd0e2f81405a814b5591dc8b", size = 3197074, upload-time = "2025-05-14T17:36:18.732Z" }, - { url = "https://files.pythonhosted.org/packages/d4/be/a766c78ec3050cb5b734c3087cd20bafd7370b0ab0c8636a87652631af1f/sqlalchemy-2.0.41-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:34ea30ab3ec98355235972dadc497bb659cc75f8292b760394824fab9cf39826", size = 3138698, upload-time = "2025-05-14T17:57:55.395Z" }, - { url = "https://files.pythonhosted.org/packages/e5/c3/245e39ec45e1a8c86ff1ac3a88b13d0457307ac728eaeb217834a3ac6813/sqlalchemy-2.0.41-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8280856dd7c6a68ab3a164b4a4b1c51f7691f6d04af4d4ca23d6ecf2261b7923", size = 3160877, upload-time = "2025-05-14T17:36:20.178Z" }, - { url = "https://files.pythonhosted.org/packages/d7/0c/cda8631405f6417208e160070b513bb752da0885e462fce42ac200c8262f/sqlalchemy-2.0.41-cp39-cp39-win32.whl", hash = "sha256:b50eab9994d64f4a823ff99a0ed28a6903224ddbe7fef56a6dd865eec9243440", size = 2089270, upload-time = "2025-05-14T18:01:41.315Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1f/f68c58970d80ea5a1868ca5dc965d154a3b711f9ab06376ad9840d1475b8/sqlalchemy-2.0.41-cp39-cp39-win_amd64.whl", hash = "sha256:5e22575d169529ac3e0a120cf050ec9daa94b6a9597993d1702884f6954a7d71", size = 2113134, upload-time = "2025-05-14T18:01:42.801Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491, upload-time = "2025-05-14T17:55:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827, upload-time = "2025-05-14T17:55:34.921Z" }, + { url = "https://files.pythonhosted.org/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224, upload-time = "2025-05-14T17:50:41.418Z" }, + { url = "https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a", size = 3230045, upload-time = "2025-05-14T17:51:54.722Z" }, + { url = "https://files.pythonhosted.org/packages/78/2f/8c14443b2acea700c62f9b4a8bad9e49fc1b65cfb260edead71fd38e9f19/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d", size = 3159357, upload-time = "2025-05-14T17:50:43.483Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b2/43eacbf6ccc5276d76cea18cb7c3d73e294d6fb21f9ff8b4eef9b42bbfd5/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", size = 3197511, upload-time = "2025-05-14T17:51:57.308Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/677c17c5d6a004c3c45334ab1dbe7b7deb834430b282b8a0f75ae220c8eb/sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f", size = 2082420, upload-time = "2025-05-14T17:55:52.69Z" }, + { url = "https://files.pythonhosted.org/packages/e9/61/e8c1b9b6307c57157d328dd8b8348ddc4c47ffdf1279365a13b2b98b8049/sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df", size = 2108329, upload-time = "2025-05-14T17:55:54.495Z" }, { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" }, ] +[[package]] +name = "starlette" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, +] + [[package]] name = "tabulate" version = "0.9.0" @@ -2144,6 +2205,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] @@ -2195,14 +2266,14 @@ wheels = [ [[package]] name = "typing-inspection" -version = "0.4.1" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +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/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, + { 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]] @@ -2223,6 +2294,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, ] +[[package]] +name = "uvicorn" +version = "0.45.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/62b0d9a2cfc8b4de6771322dae30f2db76c66dae9ec32e94e176a44ad563/uvicorn-0.45.0.tar.gz", hash = "sha256:3fe650df136c5bd2b9b06efc5980636344a2fbb840e9ddd86437d53144fa335d", size = 87818, upload-time = "2026-04-21T10:43:46.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/88/d0f7512465b166a4e931ccf7e77792be60fb88466a43964c7566cbaff752/uvicorn-0.45.0-py3-none-any.whl", hash = "sha256:2db26f588131aeec7439de00f2dd52d5f210710c1f01e407a52c90b880d1fd4f", size = 69838, upload-time = "2026-04-21T10:43:45.029Z" }, +] + [[package]] name = "watchdog" version = "6.0.0" @@ -2238,13 +2323,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, - { url = "https://files.pythonhosted.org/packages/05/52/7223011bb760fce8ddc53416beb65b83a3ea6d7d13738dde75eeb2c89679/watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8", size = 96390, upload-time = "2024-11-01T14:06:49.325Z" }, - { url = "https://files.pythonhosted.org/packages/9c/62/d2b21bc4e706d3a9d467561f487c2938cbd881c69f3808c43ac1ec242391/watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a", size = 88386, upload-time = "2024-11-01T14:06:50.536Z" }, - { url = "https://files.pythonhosted.org/packages/ea/22/1c90b20eda9f4132e4603a26296108728a8bfe9584b006bd05dd94548853/watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c", size = 89017, upload-time = "2024-11-01T14:06:51.717Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, - { url = "https://files.pythonhosted.org/packages/5b/79/69f2b0e8d3f2afd462029031baafb1b75d11bb62703f0e1022b2e54d49ee/watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa", size = 87903, upload-time = "2024-11-01T14:06:57.052Z" }, - { url = "https://files.pythonhosted.org/packages/e2/2b/dc048dd71c2e5f0f7ebc04dd7912981ec45793a03c0dc462438e0591ba5d/watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e", size = 88381, upload-time = "2024-11-01T14:06:58.193Z" }, { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, @@ -2265,12 +2348,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b66 wheels = [ { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, ] - -[[package]] -name = "zipp" -version = "3.22.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/12/b6/7b3d16792fdf94f146bed92be90b4eb4563569eca91513c8609aebf0c167/zipp-3.22.0.tar.gz", hash = "sha256:dd2f28c3ce4bc67507bfd3781d21b7bb2be31103b51a4553ad7d90b84e57ace5", size = 25257, upload-time = "2025-05-26T14:46:32.217Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/da/f64669af4cae46f17b90798a827519ce3737d31dbafad65d391e49643dc4/zipp-3.22.0-py3-none-any.whl", hash = "sha256:fe208f65f2aca48b81f9e6fd8cf7b8b32c26375266b009b413d45306b6148343", size = 9796, upload-time = "2025-05-26T14:46:30.775Z" }, -]