diff --git a/.github/workflows/docs-sphinx.yml b/.github/workflows/docs-sphinx.yml new file mode 100644 index 0000000..43ed6be --- /dev/null +++ b/.github/workflows/docs-sphinx.yml @@ -0,0 +1,29 @@ +name: Docs (Sphinx) + +on: + push: + branches: [dev] + pull_request: + branches: [dev] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup uv + uses: astral-sh/setup-uv@v5 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version-file: .python-version + + - name: Install docs dependencies + run: uv sync --extra docs --frozen + + - name: Build Sphinx HTML + run: uv run --no-sync --frozen sphinx-build -b html docs docs/_build/html -W \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8098ee4..1529a3e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,7 +3,8 @@ name: Publish to PyPI on: release: types: [published] - workflow_dispatch: null + workflow_dispatch: {} + jobs: publish: @@ -11,6 +12,10 @@ jobs: permissions: id-token: write contents: read + packages: write + + # Only publish on valid version tags + if: startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '.') environment: name: pypi @@ -42,5 +47,10 @@ jobs: - name: Build run: uv build + - name: Check build artifacts + run: uv run --no-sync --frozen twine check dist/* + - name: Publish - uses: pypa/gh-action-pypi-publish@release/v1 + uses: pypa/gh-action-pypi-publish@v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..1a2b1a7 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,17 @@ + +version: 2 + +build: + os: ubuntu-24.04 + tools: + python: "3.12" + jobs: + create_environment: + - asdf plugin add uv + - asdf install uv latest + - asdf global uv latest + install: + - uv sync --extra docs --frozen + build: + html: + - uv run sphinx-build -b html docs $READTHEDOCS_OUTPUT/html diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29..9a59691 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## 0.1.0b1 - 2026-04-08 + +### Added +- Config-relative path resolution for `PatchFile`, `SeedFile`, `NetworkFile`, and `OutputDir`. +- Built-in YAML model templates for `sir`, `seir`, and `sirs` under package templates. +- `list-models` now reports both Python reference models and YAML templates. + +### Changed +- Validation now checks transition source/target compartments explicitly. +- Validation now checks transition expression identifiers against `compartments ∪ Parameters`. +- Compartment source-of-truth reconciled: when `compartments` is provided in config, it must match `SeedFile` columns. +- Package metadata updated to `Development Status :: 4 - Beta`. +- Runtime dependencies now include conservative minimum version floors. + +### Removed +- Removed unused `patchsim.utils.loader` module and its documentation reference. diff --git a/README.md b/README.md index 1c17dcd..ee21702 100644 --- a/README.md +++ b/README.md @@ -101,14 +101,29 @@ uv run patchsim --version # Initialize a new self-contained project uv run patchsim init my-project +# Initialize with a starter template +uv run patchsim init my-project --template seir + # Validate config without running uv run patchsim validate -c my-project/config.yaml +# Print the JSON Schema for configs +uv run patchsim validate --schema + +# Emit machine-readable JSON output +uv run patchsim validate -c my-project/config.yaml --json + # Run simulation uv run patchsim run -c my-project/config.yaml -# List built-in models +# Run and emit machine-readable JSON output +uv run patchsim run -c my-project/config.yaml --json + +# List built-in model references and YAML templates uv run patchsim list-models + +# List models as JSON +uv run patchsim list-models --json ``` ### Python SDK diff --git a/docs/_static/.gitkeep b/docs/_static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/cli-reference.md b/docs/cli-reference.md new file mode 100644 index 0000000..63eb994 --- /dev/null +++ b/docs/cli-reference.md @@ -0,0 +1,276 @@ +# CLI Reference + +Command-line interface reference for PatchSim. + +## Overview + +```bash +patchsim [COMMAND] [OPTIONS] +``` + +**Available commands:** +- `init` – Scaffold a new project +- `run` – Execute a simulation +- `validate` – Check configuration and inputs +- `list-models` – Show available templates +- `--version` – Display version + +--- + +## `init` + +Initialize a new PatchSim project with starter files. + +### Usage + +```bash +patchsim init PROJECT_NAME [--template TEMPLATE] [--force] +``` + +### Arguments + +| Argument | Required | Description | +|----------|----------|-------------| +| `PROJECT_NAME` | Yes | Directory name for the new project | + +### Options + +| Option | Default | Description | +|--------|---------|-------------| +| `--template` | `sir` | Starter model template: `sir`, `seir`, `sirs`, `sis` | +| `--force` | - | Overwrite target directory if it already exists | + +### Examples + +```bash +# Create a new SIR project (default) +patchsim init my-project + +# Create with SEIR template +patchsim init my-project --template seir + +# Overwrite existing directory +patchsim init my-project --force +``` + +### Output + +Creates a directory with: +``` +config.yaml # Model configuration +data/ + patch/ # Patch population files + seeds/ # Initial conditions + networks/ # Network topology (if multi-patch) +output/ # (Empty until run) +``` + +--- + +## `validate` + +Validate configuration file and input data for consistency. + +### Usage + +```bash +patchsim validate [-c CONFIG] [--schema] [--json] +``` + +### Options + +| Option | Required | Description | +|--------|----------|-------------| +| `-c, --config` | Conditional* | Path to config YAML file | +| `--schema` | - | Print the configuration JSON Schema | +| `--json` | - | Output validation results as JSON | + +*Required unless `--schema` is used. + +### Examples + +```bash +# Validate a configuration file +patchsim validate -c config.yaml + +# Print the JSON Schema for config files +patchsim validate --schema + +# Validate and output JSON +patchsim validate -c config.yaml --json +``` + +### Output (Plain Text) + +``` +Configuration is valid: config.yaml +``` + +### Output (JSON) + +```json +{ + "ok": true, + "config": "config.yaml", + "model_name": "sample-sir-ode", + "num_patches": 1, + "patches": ["PatchA"] +} +``` + +### Validation Checks + +- All required fields present (see [Configuration](configuration.md)) +- Compartments defined and non-empty +- Transitions use valid compartments and parameters +- Patch file exists and is readable +- Seed file has correct structure +- Compartments match seed file columns +- Seed values sum to patch population +- Network file (if provided) is consistent + +--- + +## `run` + +Execute a simulation and generate outputs. + +### Usage + +```bash +patchsim run -c CONFIG [--json] +``` + +### Options + +| Option | Required | Description | +|--------|----------|-------------| +| `-c, --config` | Yes | Path to configuration YAML file | +| `--json` | - | Output run summary as JSON | + +### Examples + +```bash +# Run a simulation +patchsim run -c config.yaml + +# Run and capture output as JSON +patchsim run -c config.yaml --json +``` + +### Output (Plain Text) + +``` +Starting PatchSim simulation... +Model: sample-sir-ode +Parameters: beta=0.5, gamma=0.1 +Simulation completed successfully. +``` + +### Output (JSON) + +```json +{ + "ok": true, + "config": "config.yaml", + "model_name": "sample-sir-ode", + "output_dir": "output", + "csv_path": "output/runs/all_patches_sample-sir-ode_ode.csv", + "plot_path": "output/plots/patch_timeseries_sample-sir-ode_ode.png", + "num_patches": 1, + "patches": ["PatchA"], + "t_max": 100 +} +``` + +### Output Files + +Generates in `OutputDir` (from config): + +| File | Description | +|------|-------------| +| `runs/all_patches_MODEL_ode.csv` | Time series data (time + all compartments) | +| `plots/patch_timeseries_MODEL_ode.png` | Line plot of all compartments over time | +| `logs/MODEL_run_*.log` | Simulation log (detailed diagnostics) | + +See [Results](results.md) for output format details. + +--- + +## `list-models` + +Display available starter templates. + +### Usage + +```bash +patchsim list-models [--json] +``` + +### Options + +| Option | Description | +|--------|-------------| +| `--json` | Output model list as JSON | + +### Examples + +```bash +# List available templates +patchsim list-models + +# Output as JSON +patchsim list-models --json +``` + +### Output (Plain Text) + +``` +Built-in models and templates: +- seir (yaml-template) +- sir (yaml-template) +- sirs (yaml-template) +- sis (yaml-template) +``` + +### Output (JSON) + +```json +{ + "models": [ + {"name": "seir", "kind": "yaml-template"}, + {"name": "sir", "kind": "yaml-template"}, + {"name": "sirs", "kind": "yaml-template"}, + {"name": "sis", "kind": "yaml-template"} + ] +} +``` + +--- + +## Global Options + +### `--version` + +Display the installed PatchSim version. + +```bash +patchsim --version +``` + +Output: +``` +patchsim 0.1.0b1 +``` + +--- + +## Common Errors and Solutions + +| Error | Cause | Solution | +|-------|-------|----------| +| `Compartment mismatch` | Seed file columns don't match config | Update seed file or config compartments | +| `Unknown names in expression` | Typo in transition rate | Check parameter/compartment names in config | +| `Refusing to overwrite` | Target directory exists | Use `--force` or choose a different name | +| `Missing required field` | Config missing field (e.g., `TMax`) | See [Configuration](configuration.md) for required fields | +| `Population mismatch` | Seed values don't sum to population | Fix seed file CSV | diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..98fcecc --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,41 @@ +import os +import sys + +from pygments import lexers as pyg_lexers +from sphinx.highlighting import lexers as sphinx_lexers # type: ignore + +# Add project root to sys.path +sys.path.insert(0, os.path.abspath("..")) + +project = "patchsim" +author = "" + +extensions = [ + "myst_parser", + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", +] + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +html_theme = "pydata_sphinx_theme" +html_static_path = ["_static"] + +# MyST configuration +myst_enable_extensions = [ + "deflist", + "fieldlist", + "html_admonition", + "html_image", + "colon_fence", +] + +# Register a CSV lexer name for Pygments; use named lookup and fall back to text. +try: + lexer = pyg_lexers.get_lexer_by_name("csv") +except Exception: + lexer = pyg_lexers.get_lexer_by_name("text") + +sphinx_lexers["csv"] = lexer diff --git a/docs/configuration.md b/docs/configuration.md index 7370c3d..a811d7d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,30 +1,358 @@ # Configuration -PatchSim simulations are configured using YAML files. +PatchSim simulations are configured using YAML files. This guide documents every configuration field with examples. -## Model definition +## Configuration Structure + +A PatchSim config file has four main sections: + +1. **I/O & Metadata** – Files and output location +2. **Model Definition** – Compartments, parameters, transitions +3. **Data** – Initial conditions and network topology +4. **Simulation Control** – Time steps and solver settings + +--- + +## I/O and Metadata + +### ModelName + +**Type:** String +**Required:** Yes +**Description:** Unique identifier for the simulation. Used in output filenames. + +```yaml +ModelName: my-sir-model +``` + +### OutputDir + +**Type:** File path +**Required:** Yes +**Description:** Directory where results (CSV, plots, logs) are saved. Created if it doesn't exist. + +```yaml +OutputDir: output/results +``` + +### PatchFile + +**Type:** File path +**Required:** Yes +**Description:** CSV file defining patch (region) names and total populations. Must have columns: `patch`, `population`. + +**Example file** (`data/patch/patches.csv`): +```csv +patch,population +PatchA,1000 +PatchB,500 +``` + +```yaml +PatchFile: data/patch/patches.csv +``` + +### SeedFile + +**Type:** File path +**Required:** Yes +**Description:** CSV file with initial compartment values for each patch. Must have columns: `patch` + all compartment names. + +**Example file** (`data/seeds/initial.csv`): +```csv +patch,S,I,R +PatchA,950,50,0 +PatchB,450,50,0 +``` + +Constraints: +- One row per patch +- Compartment values must be non-negative +- Sum of values per patch must equal population in PatchFile + +```yaml +SeedFile: data/seeds/initial.csv +``` + +### NetworkFile + +**Type:** File path or `null` +**Required:** No (default: `null`) +**Description:** CSV file describing transmission links between patches. Required for multi-patch simulations. Ignored for single-patch models. + +**Example file** (`data/networks/network.csv`): +```csv +source,target,weight +PatchA,PatchB,0.2 +PatchB,PatchA,0.1 +``` + +Columns: +- `source` – Source patch (origin of transmission) +- `target` – Target patch (receives transmission) +- `weight` – Transmission weight (0–1, higher = more transmission) ```yaml -# Input files (required) -PatchFile: data/patch/patch-population.csv NetworkFile: data/networks/network.csv -SeedFile: data/seeds/seed-initial.csv - -# Model configuration -ModelName: sample-sir-ode - -# Simulation parameters -TMax: 50 -Tolerance: 1e-8 -MaxIter: 10000 -StartDate: 2020-01-01 -EndDate: 2022-12-31 -OutputDir: output/sample-sir-ode -compartments: ["S", "I", "R"] +``` + +Or for single-patch: + +```yaml +NetworkFile: null +``` + +--- + +## Model Definition + +### compartments + +**Type:** List of strings +**Required:** No (inferred from SeedFile if omitted) +**Description:** List of epidemiological state variables. If not provided, inferred from SeedFile columns. + +```yaml +compartments: + - S # Susceptible + - E # Exposed + - I # Infectious + - R # Recovered +``` + +Common compartments: +- `S` – Susceptible +- `E` – Exposed +- `I` – Infectious +- `R` – Recovered +- `D` – Deceased +- `V` – Vaccinated + +### Parameters + +**Type:** Dictionary of name → number +**Required:** Yes +**Description:** Global per-capita transition rates. Available in all transition expressions. Can be overridden per-patch in `PatchParameters`. + +**Example:** +```yaml Parameters: - beta: 0.08 + beta: 0.5 # Transmission rate (contacts per individual per day) + sigma: 0.2 # Progression rate (1 / incubation period) + gamma: 0.1 # Recovery rate (1 / infectious period) +``` + +**Key point:** Parameters are per-capita rates. See [Rate Multiplication](rate-multiplication.md) for how rates interact with compartments. + +### Transitions + +**Type:** Dictionary of `compartment -> compartment` → expression +**Required:** Yes +**Description:** Define movement between compartments using arrow syntax and mathematical expressions. + +**Syntax:** `SOURCE -> TARGET: expression` + +**Example (SIR):** +```yaml +Transitions: + S -> I: "beta * S * I" # New infections + I -> R: "gamma * I" # Recoveries +``` + +**Example (SEIR):** +```yaml +Transitions: + S -> E: "beta * S * I" # Infection + E -> I: "sigma * E" # Progression to infectious + I -> R: "gamma * I" # Recovery +``` + +**Expression rules:** +- Use compartment names (e.g., `S`, `I`) for counts +- Use parameter names (e.g., `beta`, `gamma`) for rates +- Python operators allowed: `*`, `+`, `-`, `/`, `**`, `and`, `or` +- Only identifiers that are compartments or parameters allowed +- Rates are automatically multiplied by source compartment (see [Rate Multiplication](rate-multiplication.md)) + +**Valid expressions:** +```yaml +S -> I: "beta" # Rate multiplied by S automatically +S -> I: "beta * I" # Manual interaction term +E -> I: "sigma" +I -> R: "gamma" +R -> S: "rho" # Waning immunity +``` + +**Invalid expressions:** +```yaml +S -> I: "unknown_param" # Error: unknown parameter +S -> I: "S / 0" # Error: division issues caught at runtime +``` + +--- + +## Patch-Specific Parameters (Optional) + +### PatchParameters + +**Type:** Dictionary of patch name → parameter overrides +**Required:** No +**Description:** Override global parameters for specific patches. Useful for spatial heterogeneity. + +```yaml +Parameters: + beta: 0.5 + gamma: 0.1 + +PatchParameters: + PatchA: + beta: 0.8 # Higher transmission in PatchA + PatchB: + beta: 0.3 # Lower transmission in PatchB +``` + +Constraints: +- Keys must match patch names from PatchFile +- Only override parameters you need to change +- Overrides are merged with global Parameters + +--- + +## Simulation Control + +### TMax + +**Type:** Positive integer +**Required:** Yes +**Description:** Number of time steps to simulate. + +```yaml +TMax: 100 +``` + +This simulates from time 0 to 99 (100 steps total). + +--- + +## Complete Examples + +### Minimal SIR (Single Patch) + +```yaml +ModelName: simple-sir +OutputDir: output/sir +PatchFile: data/patch/single.csv +SeedFile: data/seeds/sir-init.csv +NetworkFile: null + +compartments: + - S + - I + - R + +Parameters: + beta: 0.5 + gamma: 0.1 + +Transitions: + S -> I: "beta" + I -> R: "gamma" + +TMax: 100 +``` + +### SEIR with Waning Immunity + +```yaml +ModelName: seir-waning +OutputDir: output/seir +PatchFile: data/patch/patches.csv +SeedFile: data/seeds/seir-init.csv +NetworkFile: null + +compartments: + - S + - E + - I + - R + +Parameters: + beta: 0.5 # Transmission rate + sigma: 0.2 # Progression (1/latency) + gamma: 0.1 # Recovery (1/infectious) + rho: 0.01 # Waning immunity + +Transitions: + S -> E: "beta * I" # Exposure + E -> I: "sigma" # Progression + I -> R: "gamma" # Recovery + R -> S: "rho" # Loss of immunity + +TMax: 365 +``` + +### Multi-Patch with Heterogeneous Transmission + +```yaml +ModelName: network-sir +OutputDir: output/network +PatchFile: data/patch/regions.csv +SeedFile: data/seeds/network-init.csv +NetworkFile: data/networks/routes.csv + +compartments: + - S + - I + - R + +Parameters: + beta: 0.3 gamma: 0.1 + +PatchParameters: + Urban: + beta: 0.6 # Higher transmission in cities + Rural: + beta: 0.2 # Lower transmission in rural areas + Transitions: - "S -> I": "beta" - "I -> R": "gamma * I" -``` \ No newline at end of file + S -> I: "beta * I" + I -> R: "gamma" + +TMax: 200 +``` + +--- + +## Validation + +Before running, validate your configuration: + +```bash +patchsim validate -c config.yaml +``` + +This checks: +- ✅ All required fields present +- ✅ Files exist and are readable +- ✅ Compartments match seed file +- ✅ Transitions reference valid compartments/parameters +- ✅ Seed values are consistent + +### Common Validation Errors + +| Error | Cause | Fix | +|-------|-------|-----| +| `Missing required field 'TMax'` | TMax not in config | Add `TMax: N` | +| `Compartment mismatch` | Seed columns ≠ compartments | Align seed file with compartments | +| `Unknown names in expression` | Typo in transition | Check parameter/compartment spelling | +| `Refusing to overwrite` | OutputDir exists | Use different directory or `--force` | + +--- + +## Next Steps + +- **[Getting Started](getting-started.md)** – Hands-on walkthrough +- **[CLI Reference](cli-reference.md)** – Command documentation +- **[Mathematical Model](mathematical-model.md)** – Equations and rate multiplication +- **[Network Design](network-design.md)** – Multi-patch topology diff --git a/docs/getting-started.md b/docs/getting-started.md index a099e8f..f3982ce 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,23 +1,177 @@ - - # Getting Started +A quick hands-on walkthrough from installation to your first simulation results. + ## Installation ### From PyPI (Recommended) -Install patchsim from PyPI: - ```bash pip install patchsim ``` ### For Contributors -Clone the repository and install in editable mode for development: +Clone the repository and install in editable mode: ```bash git clone https://github.com/dsih-artpark/patchsim.git cd patchsim -pip install -e . -``` \ No newline at end of file +pip install -e ".[dev]" +``` + +## Your First Simulation (5 minutes) + +### Step 1: Create a Project + +Initialize a new PatchSim project with the SIR template: + +```bash +patchsim init my-first-sim +cd my-first-sim +ls +``` + +You should see: + +``` +config.yaml # Model and simulation configuration +data/ + networks/ # Network files (transmission between patches) + patch/ # Patch populations + seeds/ # Initial compartment values +output/ # (Created when you run the simulation) +``` + +### Step 2: Examine the Configuration + +Open `config.yaml` to see the model structure: + +```yaml +ModelName: sample-sir-ode +TMax: 100 + +PatchFile: data/patch/sample-sir-ode-patch-population.csv +SeedFile: data/seeds/sample-sir-ode-patchA-2.csv +NetworkFile: null # Single patch, no network +OutputDir: output + +# Model compartments (S, I, R) +compartments: + - S + - I + - R + +# Global parameters (all patches use these unless overridden) +Parameters: + beta: 0.5 # Transmission rate + gamma: 0.1 # Recovery rate + +# Transitions: how individuals move between compartments +Transitions: + S -> I: beta * S * I # New infections + I -> R: gamma * I # Recoveries +``` + +**Key concepts:** +- **Compartments:** The state variables (S=Susceptible, I=Infectious, R=Recovered) +- **Parameters:** Per-capita rates (beta=contacts/day, gamma=1/duration) +- **Transitions:** Define movement between compartments using rate expressions +- **Rate multiplication rule:** `flow = rate × source_compartment`. See [Rate Multiplication](rate-multiplication.md) for details. + +### Step 3: Validate Your Configuration + +Before running, check that inputs are consistent: + +```bash +patchsim validate -c config.yaml +``` + +This checks: +- ✅ All required fields present +- ✅ Compartments match seed file columns +- ✅ Transitions reference valid compartments and parameters +- ✅ Patch populations match seed values +- ✅ Network file (if provided) is consistent + +### Step 4: Run the Simulation + +Execute the simulation: + +```bash +patchsim run -c config.yaml +``` + +You should see logging output: + +``` +Starting PatchSim simulation... +Model: sample-sir-ode +Parameters: beta=0.5, gamma=0.1 +Simulation completed successfully. +``` + +### Step 5: Examine Results + +Check the output directory: + +```bash +ls output/runs/ # CSV file with time series +ls output/plots/ # PNG plots of compartments over time +``` + +**CSV output** (`output/runs/all_patches_sample-sir-ode_ode.csv`): + +```csv +time,S_0,I_0,R_0 +0.0,990,10,0 +1.0,975.2,20.1,4.7 +2.0,958.4,28.9,12.7 +... +``` + +**PNG plots** show S, I, R trajectories for each patch. + +### Step 6: Modify the Model + +Now change the model to SEIR (add exposed compartment): + +```bash +patchsim init my-seir --template seir +cd my-seir +patchsim validate -c config.yaml +patchsim run -c config.yaml +``` + +The SEIR template adds an E (exposed) compartment and demonstrates model customization. See [Configuration](configuration.md) for all fields. + +## JSON Output Mode + +For integration with pipelines, use `--json` flags: + +```bash +# Validate and get machine-readable output +patchsim validate -c config.yaml --json + +# Run and get summary in JSON +patchsim run -c config.yaml --json + +# List available model templates +patchsim list-models --json +``` + +## Next Steps + +- **[Configuration Reference](configuration.md)** – All config fields and options +- **[CLI Reference](cli-reference.md)** – Command reference and examples +- **[Mathematical Model](mathematical-model.md)** – Equations and how rate multiplication works +- **[Network Design](network-design.md)** – Setting up multi-patch simulations +- **[Results](results.md)** – Understanding and analyzing outputs + +## Troubleshooting + +**"Compartment mismatch"** – Ensure seed file columns match configured compartments +**"Unknown names in expression"** – Check for typos in transition rate expressions +**"Refusing to overwrite"** – Use `patchsim init --force` to override an existing directory + +For more help, open an issue on [GitHub](https://github.com/dsih-artpark/patchsim). diff --git a/docs/index.md b/docs/index.md index b0c42d8..6335d9d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,27 +1,91 @@ +--- +title: PatchSim documentation +--- + +# PatchSim documentation + +```{toctree} +:maxdepth: 2 + +getting-started.md +configuration.md +mathematical-model.md +network-design.md +simulation-workflow.md +results.md +cli-reference.md +rate-multiplication.md + + +``` # PatchSim -PatchSim is a modular, general-purpose metapopulation epidemiological simulation framework designed for research and translation use cases. It is a flexible and modular system for simulating epidemiological compartment models across single or multiple interacting patches. - -The framework generalizes classical compartmental models (e.g. SIR) by allowing multiple subpopulations ("patches") to interact through a weighted directed network. - -PatchSim emphasizes: -- Explicit separation between patch dynamics and network structure -- Continuous-time ODE-based simulation -- Configurable parameters via YAML -- Extensibility toward richer network and mobility models - -## Features - -- **Flexible Compartment Models**: Define custom SIR, SEIR, or other compartmental models that support multiple diseases and problem types -- **Multi-Patch Networks**: Simulate disease spread across connected geographical regions -- **CLI Integration**: Integrated CLI for running models -- **Multiple Simulation Methods**: Support for both ODE and discrete-time simulation modes -- **YAML Configuration**: Structured, readable configuration files -- **Per-Patch Parameters**: Built-in reproducibility and management of different parameters for each patch -- **Network Connectivity**: Define custom mixing matrices between patches -- **Extensibility**: Extensible and user-friendly - -## Documentation -- [Core Module](reference/core.md) -- [Models](reference/models.md) -- [Utilities](reference/utils.md) +PatchSim is a modular metapopulation simulation framework for compartmental epidemiology. +It supports single-patch and multi-patch models, YAML-based configuration, and ODE-based +simulation with network-coupled transmission. + +## What PatchSim provides + +- **Compartment models**: SIR, SEIR, SIRS, SIS, and custom variants +- **Multi-patch networks**: Weighted directed patch-to-patch transmission +- **YAML configuration**: Model structure, parameters, seed data, and network inputs +- **CLI workflows**: Scaffold, validate, run, inspect models, and export summaries +- **Results outputs**: CSV time series, plots, and logs + +## Start here + +If you are new to the project, follow the docs in this order: + +1. [Getting Started](getting-started.md) — install, initialize, validate, and run +2. [Configuration](configuration.md) — every YAML field and a worked SEIR example +3. [CLI Reference](cli-reference.md) — command and flag reference +4. [Mathematical Model](mathematical-model.md) — equations and the rate rule +5. [Network Design](network-design.md) — how multi-patch mixing works +6. [Results](results.md) — how to interpret CSVs, plots, and logs + +## Core concepts + +### Compartments + +Compartments represent model states such as `S`, `E`, `I`, and `R`. + +### Parameters + +Parameters are per-capita rates like `beta`, `sigma`, `gamma`, and `rho`. + +### Transitions + +Transitions define movement between compartments using YAML expressions. +See [Mathematical Model](mathematical-model.md) for the rate multiplication rule. + +### Network structure + +Patch-to-patch transmission is represented by a weighted directed network. +See [Network Design](network-design.md) for the expected CSV format. + +## Common workflows + +- Use [Getting Started](getting-started.md) for a fast first run +- Use [Configuration](configuration.md) to edit model behavior +- Use [CLI Reference](cli-reference.md) to see flags and outputs +- Use [Results](results.md) to interpret output files + +## Output overview + +PatchSim writes results to the configured `OutputDir`: + +- `runs/` — time series CSV output +- `plots/` — visualization images +- `logs/` — run logs with configuration and solver details + +## Documentation map + +- [Getting Started](getting-started.md) +- [Configuration](configuration.md) +- [CLI Reference](cli-reference.md) +- [Mathematical Model](mathematical-model.md) +- [Rate Multiplication](rate-multiplication.md) +- [Network Design](network-design.md) +- [Simulation Workflow](simulation-workflow.md) +- [Architecture](architecture.md) +- [Results](results.md) diff --git a/docs/mathematical-model.md b/docs/mathematical-model.md index 7774a7e..2016003 100644 --- a/docs/mathematical-model.md +++ b/docs/mathematical-model.md @@ -1,25 +1,323 @@ # Mathematical Model -PatchSim implements compartmental models using continuous-time ODEs. +PatchSim implements continuous-time compartmental disease models using ordinary differential equations (ODEs). -## Network-coupled SIR model +## Key Principle: Rate Multiplication + +**The foundational rule:** In any transition, the flow is the per-capita rate multiplied by the source compartment count. + +For a transition `A -> B` with rate expression $r$: + +$$\frac{dA}{dt} = -r \cdot A, \quad \frac{dB}{dt} = +r \cdot A$$ + +**Important:** You only specify the per-capita rate in your transition expression. PatchSim automatically multiplies by the source compartment. This means: + +```yaml +Transitions: + S -> I: "beta" # Not "beta * S" + I -> R: "gamma" # Not "gamma * I" +``` + +The framework assumes: +- `beta` is the per-capita transmission rate +- `gamma` is the per-capita recovery rate +- Flow is calculated as $\text{rate} \times \text{source compartment}$ + +See [Rate Multiplication](rate-multiplication.md) for detailed explanation and examples. + +--- + +## Single-Patch SIR Model + +The basic susceptible-infectious-recovered model for a single patch. + +### Compartments + +- **S** – Susceptible individuals +- **I** – Infectious individuals +- **R** – Recovered individuals + +### Parameters + +- **β** – Per-capita transmission rate (contacts per individual per day) +- **γ** – Per-capita recovery rate (1 / infectious period) + +### Equations + +$$\frac{dS}{dt} = -\beta S I$$ + +$$\frac{dI}{dt} = \beta S I - \gamma I$$ + +$$\frac{dR}{dt} = \gamma I$$ + +### Configuration + +```yaml +compartments: + - S + - I + - R + +Parameters: + beta: 0.5 # Transmission rate + gamma: 0.1 # Recovery rate (so 1/gamma = 10 days infectious) + +Transitions: + S -> I: "beta * I" # Bimolecular: susceptible × infectious + I -> R: "gamma" # Unimolecular: infected recover +``` + +--- + +## Multi-Patch Network-Coupled SIR + +For spatial transmission between patches. + +### Setup Let: -- S_i, I_i, R_i be compartments of patch i -- N_i = S_i + I_i + R_i -- W_ij be the network weight from source patch j to target patch i (i.e., W_ij denotes the weight of transmission from j to i) +- $S_i, I_i, R_i$ = compartments in patch $i$ +- $N_i = S_i + I_i + R_i$ = total population in patch $i$ +- $W_{ij}$ = network weight from source patch $j$ to target patch $i$ (how much transmission flows from j to i) + +### Force of Infection + +The per-capita force of infection in patch $i$ aggregates infectious pressure from all patches: + +$$\lambda_i(t) = \sum_{j=1}^{n} W_{ij} \frac{I_j}{N_j}$$ + +This represents the probability that a susceptible in patch $i$ contacts an infected individual from patch $j$. + +### Equations + +$$\frac{dS_i}{dt} = -\beta S_i \lambda_i(t)$$ + +$$\frac{dI_i}{dt} = \beta S_i \lambda_i(t) - \gamma I_i$$ + +$$\frac{dR_i}{dt} = \gamma I_i$$ + +### Configuration + +```yaml +compartments: + - S + - I + - R + +Parameters: + beta: 0.5 + gamma: 0.1 + +Transitions: + S -> I: "beta * I" + I -> R: "gamma" + +NetworkFile: data/networks/network.csv +``` + +**Network file** (`data/networks/network.csv`): +```csv +source,target,weight +PatchA,PatchB,0.2 +PatchB,PatchA,0.1 +``` + +Network weights can be: +- 0 (no connection) +- 0–1 (partial transmission) +- ≥1 (if modeling strong links, e.g., commuting amplification) + +--- + +## SEIR Model (With Latency) + +Adds an exposed (latent) compartment for disease incubation. + +### Compartments + +- **S** – Susceptible +- **E** – Exposed (infected but not yet infectious) +- **I** – Infectious +- **R** – Recovered + +### Parameters + +- **β** – Per-capita transmission rate +- **σ** – Per-capita progression rate (1 / incubation period) +- **γ** – Per-capita recovery rate + +### Equations + +$$\frac{dS}{dt} = -\beta S I$$ + +$$\frac{dE}{dt} = \beta S I - \sigma E$$ + +$$\frac{dI}{dt} = \sigma E - \gamma I$$ + +$$\frac{dR}{dt} = \gamma I$$ + +### Configuration + +```yaml +compartments: + - S + - E + - I + - R + +Parameters: + beta: 0.5 # Transmission rate + sigma: 0.2 # Progression (1/incubation, typically 1–5 days) + gamma: 0.1 # Recovery (1/infectious) + +Transitions: + S -> E: "beta * I" + E -> I: "sigma" + I -> R: "gamma" +``` + +--- + +## SIRS Model (With Waning Immunity) + +Susceptible → Infectious → Recovered → Susceptible (loss of immunity). + +### Compartments + +- **S** – Susceptible +- **I** – Infectious +- **R** – Recovered (with temporary immunity) + +### Parameters + +- **β** – Per-capita transmission rate +- **γ** – Per-capita recovery rate +- **ρ** – Per-capita waning immunity rate + +### Equations + +$$\frac{dS}{dt} = -\beta S I + \rho R$$ + +$$\frac{dI}{dt} = \beta S I - \gamma I$$ + +$$\frac{dR}{dt} = \gamma I - \rho R$$ + +### Configuration + +```yaml +compartments: + - S + - I + - R + +Parameters: + beta: 0.5 # Transmission + gamma: 0.1 # Recovery + rho: 0.01 # Waning immunity (1/duration of immunity) + +Transitions: + S -> I: "beta * I" + I -> R: "gamma" + R -> S: "rho" +``` + +--- + +## SIS Model (Acute, No Immunity) + +For diseases without acquired immunity (e.g., some bacterial infections). + +### Compartments + +- **S** – Susceptible +- **I** – Infectious + +### Parameters + +- **β** – Per-capita transmission rate +- **γ** – Per-capita recovery rate (to susceptible, not recovered) + +### Equations + +$$\frac{dS}{dt} = -\beta S I + \gamma I$$ + +$$\frac{dI}{dt} = \beta S I - \gamma I$$ + +### Configuration + +```yaml +compartments: + - S + - I + +Parameters: + beta: 0.5 + gamma: 0.1 + +Transitions: + S -> I: "beta * I" + I -> S: "gamma" +``` + +--- + +## Custom Models + +You can define any compartmental model by: + +1. **List your compartments** (e.g., S, V, E, I, H, D) +2. **Define per-capita rates** as Parameters +3. **Specify transitions** using the `SOURCE -> TARGET: rate` syntax +4. **Provide initial conditions** in SeedFile + +Example: SEIRD (with deaths) + +```yaml +compartments: + - S + - E + - I + - R + - D + +Parameters: + beta: 0.5 + sigma: 0.2 + gamma: 0.09 # Recovery rate + mu: 0.01 # Case fatality rate + +Transitions: + S -> E: "beta * I" + E -> I: "sigma" + I -> R: "gamma" + I -> D: "mu" +``` + +--- + +## Numerical Integration + +PatchSim solves ODEs using **scipy.integrate.odeint** (implicit multistep solver). + +- Default tolerance: good for most applications +- Time stepping: automatic (user specifies total steps as `TMax`) +- Assumes continuous-time dynamics over discrete reporting intervals -The force of infection for patch i is: +--- -λ_i(t) = Σ_j W_ij * (I_j / N_j) +## Key Assumptions -where j is the source patch and i is the target patch, matching the CSV weight matrix ordering (target, source). +1. **Well-mixed patches** – Within a patch, contacts are random and uniform +2. **Compartments are continuous** – Populations treated as real numbers (not individuals) +3. **Rates are constant** – Parameters don't change with time (except per-patch overrides) +4. **No vital dynamics** – No births or deaths (except disease-related) +5. **Markovian** – Next state depends only on current state, not history -The resulting dynamics are: +--- -dS_i/dt = -β S_i λ_i -dI_i/dt = β S_i λ_i - γ I_i -dR_i/dt = γ I_i +## Next Steps -This formulation reduces to the classical SIR model when the network -contains a single patch with W_ii = 1. +- **[Rate Multiplication](rate-multiplication.md)** – Deep dive into how rates work +- **[Configuration](configuration.md)** – Setting up your model +- **[Network Design](network-design.md)** – Multi-patch topology +- **[Getting Started](getting-started.md)** – Hands-on examples diff --git a/docs/rate-multiplication.md b/docs/rate-multiplication.md new file mode 100644 index 0000000..1d49032 --- /dev/null +++ b/docs/rate-multiplication.md @@ -0,0 +1,82 @@ +# Rate Multiplication Rule + +## Overview + +PatchSim uses a **rate multiplication** approach to scale transition rates based on compartment sizes or population dynamics. This section documents how rates are computed and applied across compartmental models. + +## Basic Principle + +In compartmental epidemiological models, transition rates represent the per-capita rate at which individuals move from one compartment to another. The actual number of individuals transitioning in a time step is computed by multiplying this per-capita rate by the number of individuals in the source compartment. + +### Mathematical Formulation + +For a transition from compartment A to compartment B: + +$$\text{Flow}_{A \to B} = \text{rate}_{A \to B} \times n_A$$ + +where: +- $\text{Flow}_{A \to B}$ is the number of individuals transitioning per unit time +- $\text{rate}_{A \to B}$ is the per-capita transition rate (e.g., recovery rate, transmission rate) +- $n_A$ is the current population in compartment A + +## Application in PatchSim + +### Contact-Based Transmission + +For disease transmission, the force of infection is computed as a weighted sum of infectious populations across patches: + +$$\lambda_i(t) = \beta \sum_j W_{ij} \frac{I_j}{N_j}$$ + +where: +- $\lambda_i$ is the per-capita force of infection in patch i +- $\beta$ is the transmission rate +- $W_{ij}$ is the network weight from source patch j to target patch i +- $I_j$ is the number of infectious individuals in source patch j +- $N_j$ is the total population in source patch j + +The actual number of new infections is then: + +$$dS_i = -\lambda_i \times S_i \, dt$$ + +### Recovery and Other Transitions + +For non-transmission transitions (recovery, loss of immunity, disease progression), the rate multiplication is simpler: + +$$\text{Flow} = \text{rate} \times \text{compartment\_size}$$ + +For example: +$$dI = -\gamma \times I \, dt$$ + +where $\gamma$ is the per-capita recovery rate. + +## Implementation Details + +### Parameter-Based Rate Scaling + +Parameters defined in the `Parameters` section of the configuration are used directly as per-capita rates: + +```yaml +Parameters: + beta: 0.5 # Transmission rate (contacts per individual per day) + gamma: 0.1 # Recovery rate (1/disease duration) + sigma: 0.2 # Rate of progression E → I +``` + +These rates multiply the compartment sizes during ODE integration. + +### Validation in PatchSim + +PatchSim validates that all transition expressions reference valid compartments and parameters. This ensures that rate multiplication is applied consistently across the model. + +## Common Pitfalls + +1. **Forgetting compartment size**: If you want a fixed number of individuals to transition (not rate-based), you must normalize by population size in the configuration or code. + +2. **Mixing rates and absolute numbers**: All transition rates in PatchSim are assumed to be per-capita. Do not mix per-capita rates with fixed transition counts in the same model. + +3. **Network weight interpretation**: Network weights are applied to the compartment proportion, not the absolute number. This ensures that transmission scales appropriately with patch population sizes. + +## References + +- [Mathematical Model](mathematical-model.md) for detailed network-coupled ODE formulations +- [Configuration Guide](configuration.md) for specifying parameters and transitions diff --git a/docs/reference/core.md b/docs/reference/core.md deleted file mode 100644 index 996147b..0000000 --- a/docs/reference/core.md +++ /dev/null @@ -1,11 +0,0 @@ -# Core Module - -This module contains the base classes and simulation engine for PatchSim. - -::: patchsim.core.model - -## Simulation - -Implements the simulation loop, time progression, and overall management of model execution. - -::: patchsim.core.simulation \ No newline at end of file diff --git a/docs/reference/models.md b/docs/reference/models.md deleted file mode 100644 index 9a2a65b..0000000 --- a/docs/reference/models.md +++ /dev/null @@ -1,57 +0,0 @@ -# Model Implementations - -This section documents concrete epidemiological models implemented using the PatchSim framework. - -Unlike the core modules, models are **not reusable APIs** but -self-contained reference implementations that demonstrate how to -instantiate and run simulations using the framework. - ---- - -## Design Philosophy - -PatchSim separates: - -- **Model definition** (compartments, transitions, parameters) -- **Simulation engine** (ODE solvers, network coupling) -- **Execution scripts** (models in this directory) - -Files in `patchsim.models` serve as: -- Validation cases -- Reference implementations -- Templates for new models - ---- - -## ka_fmd_sirsv_discrete - -**File:** -`patchsim/models/ka_fmd_sirsv_discrete.py` - -**Description:** -A discrete-time, multi-compartment SIRSV-style model designed for -foot-and-mouth disease (FMD) dynamics. - -**Key features:** -- Discrete-time update rules -- Multiple infectious states -- Demonstrates non-ODE simulation capability -- Designed for Karnataka (KA) regional modeling - -**Usage pattern:** -- Defines compartments and transitions explicitly -- Uses PatchSim core utilities for execution -- Can be adapted into YAML-driven workflows - ---- - -## Extending Models - -To create a new model: -1. Copy an existing model script -2. Modify compartments and transitions -3. Adjust parameters or network structure -4. Run via the `patchsim` CLI using `uv run`, for example `uv run patchsim run -c config.yaml` - -For most use cases, users are encouraged to prefer -**YAML-based model definitions** over Python scripts. diff --git a/docs/reference/utils.md b/docs/reference/utils.md deleted file mode 100644 index 41b4c9f..0000000 --- a/docs/reference/utils.md +++ /dev/null @@ -1,24 +0,0 @@ -# Utilities Module - -This module provides various utility functions used across PatchSim for input/output handling, logging, and visualisation. - -## Input Handling - -Handles reading, parsing, and setting up configuration, network, and input files. -Also includes random seed initialisation utilities. - -::: patchsim.utils.loader - -## Logger - -Provides a logging setup for the simulation runs. - -::: patchsim.utils.logger - -## Visualisation - -Functions for visualising simulation outputs, including plotting and analysis tools. - -::: patchsim.utils.viz - - diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt new file mode 100644 index 0000000..4f952b4 --- /dev/null +++ b/docs/requirements-docs.txt @@ -0,0 +1,3 @@ +sphinx>=6.0 +myst-parser>=0.18 +pydata-sphinx-theme>=0.14 diff --git a/docs/results.md b/docs/results.md index e4cb225..dab751a 100644 --- a/docs/results.md +++ b/docs/results.md @@ -1,10 +1,228 @@ -# Results +# Understanding Results -Simulations of two network-coupled patches show: +This guide explains how to interpret PatchSim simulation outputs. -- Faster epidemic spread compared to isolated patches -- Synchronization between patches -- Conservation of total population within each patch +--- -These behaviors arise from network-mediated infection pressure and -continuous-time integration. +## Output Directory Structure + +After running `patchsim run -c config.yaml`, your `OutputDir` contains: + +``` +output/ +├── runs/ +│ └── all_patches_MODEL_ode.csv # Time series data (main output) +├── plots/ +│ └── patch_timeseries_MODEL_ode.png # Visualization +└── logs/ + └── MODEL_run_YYYYMMDD_HHMMSS.log # Detailed simulation log +``` + +--- + +## CSV Output (`runs/`) + +### File Format + +The CSV file contains time series for all compartments across all patches. + +**Columns:** +- `time` – Time step (0, 1, 2, ..., TMax-1) +- `COMPARTMENT_PATCH` – Values for each compartment-patch combination + +**Example:** SIR with 2 patches + +``` +time,S_0,I_0,R_0,S_1,I_1,R_1 +0.0,950,50,0,475,25,0 +1.0,937.2,59.1,3.7,463.8,32.8,3.4 +2.0,923.1,67.2,9.7,451.2,39.4,9.4 +... +99.0,2.1,0.3,997.6,1.2,0.1,498.7 +``` + +### Interpreting Values + +- **Early (t ~ 0–20):** Growth phase. Infectious cases rising, susceptible declining. +- **Mid (t ~ 20–50):** Peak phase. Cases plateau or decline. Recovery accelerates. +- **Late (t ~ 50+):** Endemic equilibrium. Susceptible, infectious, recovered settle. + +**Key metrics from CSV:** + +| Metric | Calculation | Meaning | +|--------|------------|---------| +| **Final S** | Last value in S column | How many never got infected | +| **Max I** | Maximum across all I columns | Peak infections (key for healthcare burden) | +| **Time of max I** | Timestamp of peak | When to expect peak demand | +| **Final R** | Last value in R column | Total infected (across outbreak) | +| **Attack rate** | Final R / initial N | Proportion of population infected | + +### Multi-Patch Analysis + +For network models, compare patches to identify: +- **Leading patches:** Higher I earlier (index cases / hubs) +- **Trailing patches:** Delayed peaks (transmission lag) +- **Heterogeneous outcomes:** Some patches have more infections due to network structure + +--- + +## PNG Plots (`plots/`) + +### What's Shown + +Line plot with: +- **X-axis:** Time +- **Y-axis:** Population count +- **One subplot per patch** +- **One line per compartment** (color-coded) + +### Interpreting Plots + +- **S line** (usually blue) → Decreases as people become infected +- **I line** (usually red) → Shows epidemic curve (peaks then declines) +- **R line** (usually green) → Increases over time (cumulative recovered) + +**Example scenarios:** + +| Pattern | Meaning | +|---------|---------| +| Sharp I peak early | Rapid spread (high β, dense network) | +| Flat I peak | Slow spread (low β, isolated patches) | +| Long tail in I | Chronic endemicity (SIRS-like behavior) | +| S → 0 | Epidemic exhausted susceptible pool | + +### Multi-Patch Plots + +For network models, subplots show: +- **Urban patch:** Usually steeper I peak (more contacts) +- **Rural patch:** Usually delayed, flatter peak (fewer contacts) +- **Synchrony:** Patches with high network weight peak closer in time + +--- + +## Logs (`logs/`) + +### What's Recorded + +Each simulation generates a detailed log including: + +- **Configuration summary** – Model name, parameters, compartments, transitions +- **Data validation** – Patches loaded, seed values confirmed +- **Solver progress** – Time steps completed, solver diagnostics +- **Output paths** – Where CSV/plots were saved +- **Warnings/errors** – Any issues encountered (but simulation completed) + +### When to Check Logs + +- **Simulation failed** – Log explains what went wrong +- **Results seem wrong** – Log shows if data was loaded correctly +- **Slow simulation** – Log shows solver iteration counts +- **Production runs** – Archive logs for reproducibility + +### Example Log Snippet + +``` +2026-04-14 10:30:45 INFO Starting PatchSim simulation... +2026-04-14 10:30:45 INFO Model: sample-sir-ode +2026-04-14 10:30:45 INFO Compartments: S, I, R +2026-04-14 10:30:45 INFO Parameters: beta=0.5, gamma=0.1 +2026-04-14 10:30:45 INFO Transitions: S->I: beta*I, I->R: gamma +2026-04-14 10:30:45 INFO Loading patches from data/patch/sample.csv +2026-04-14 10:30:45 INFO Loaded patches: PatchA (1000 individuals) +2026-04-14 10:30:45 INFO Loading seeds from data/seeds/initial.csv +2026-04-14 10:30:45 INFO Seed validation: OK +2026-04-14 10:30:45 INFO Running ODE solver for 100 time steps... +2026-04-14 10:30:46 INFO Simulation completed successfully +2026-04-14 10:30:46 INFO Saved to: output/runs/all_patches_sample-sir-ode_ode.csv +``` + +--- + +## Analysis Workflow + +### 1. Quick Sanity Check + +- Open CSV and spot-check first/last rows + - S values should decrease over time + - R values should increase over time (or stay flat for SIS) + - Sum S + I + R should equal population (within rounding) + +- View PNG plot + - Does epidemic curve look reasonable for your parameters? + - Are compartment trajectories monotonic as expected? + +### 2. Extract Key Metrics + +```python +import pandas as pd + +df = pd.read_csv('output/runs/all_patches_sample-sir-ode_ode.csv') + +# Attack rate (% infected) +attack_rate = df['R_0'].iloc[-1] / 1000 + +# Peak infections +peak_I = df['I_0'].max() +peak_time = df.loc[df['I_0'].idxmax(), 'time'] + +# Final state +final_S = df['S_0'].iloc[-1] +final_I = df['I_0'].iloc[-1] +final_R = df['R_0'].iloc[-1] + +print(f"Attack rate: {attack_rate:.1%}") +print(f"Peak infections: {peak_I:.0f} at time {peak_time:.0f}") +print(f"Final state: S={final_S:.0f}, I={final_I:.0f}, R={final_R:.0f}") +``` + +### 3. Compare Scenarios + +Run multiple simulations (e.g., different β values) and compare: + +```python +import matplotlib.pyplot as plt + +scenarios = [ + ('low_transmission.csv', 'β=0.3'), + ('baseline.csv', 'β=0.5'), + ('high_transmission.csv', 'β=0.7'), +] + +fig, ax = plt.subplots() +for fname, label in scenarios: + df = pd.read_csv(f'output/runs/{fname}') + ax.plot(df['time'], df['I_0'], label=label) + +ax.set_xlabel('Time') +ax.set_ylabel('Infectious count') +ax.legend() +plt.show() +``` + +--- + +## Common Questions + +**Q: Why do my compartments have fractional values?** +A: Compartments are continuous (not individual counts). This is standard in ODE models. Round to integers if needed for reporting. + +**Q: Sum of S+I+R doesn't equal population exactly.** +A: Rounding error from ODE solver. Typically <0.01% relative error. If larger, check your seed file. + +**Q: I see negative values in the CSV.** +A: Solver numerical issue (very rare). Check your rates and initial conditions; they should all be positive. + +**Q: Compartment values don't match my expected model.** +A: Check that your transition expressions are correct. See [Configuration](configuration.md) for syntax. + +**Q: How do I export results to another format (Excel, JSON, etc.)?** +A: CSV can be imported into any tool. Use `patchsim run --json` for machine-readable summary. + +--- + +## Next Steps + +- **[Configuration](configuration.md)** – Adjust model and parameters for different scenarios +- **[Network Design](network-design.md)** – Multi-patch simulations to explore spatial dynamics +- **[Mathematical Model](mathematical-model.md)** – Understand the equations behind the numbers +- **[Getting Started](getting-started.md)** – Run more examples diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 0290719..0000000 --- a/mkdocs.yml +++ /dev/null @@ -1,76 +0,0 @@ - -site_name: Compartmental Multi-Patch Simulation Framework -site_description: Documentation for a modular metapopulation simulation framework for multi-disease epidemiological modelling. -site_author: Shreya Mukherjee -repo_url: https://github.com/dsih-artpark/patchsim -repo_name: GitHub - -theme: - name: material - palette: - # Light mode - - scheme: default - primary: indigo - accent: indigo - toggle: - icon: material/brightness-7 - name: Switch to dark mode - # Dark mode - - scheme: slate - primary: indigo - accent: indigo - toggle: - icon: material/brightness-4 - name: Switch to light mode - features: - - navigation.tabs - - navigation.sections - - navigation.expand - - navigation.top - - search.suggest - - search.highlight - - content.code.copy - - content.code.annotate - -plugins: - - search - - mkdocstrings: - default_handler: python - handlers: - python: - paths: ["src"] - options: - docstring_style: google - show_source: true - -markdown_extensions: - - pymdownx.highlight: - anchor_linenums: true - - pymdownx.inlinehilite - - pymdownx.snippets - - pymdownx.superfences - - pymdownx.tabbed: - alternate_style: true - - admonition - - pymdownx.details - - tables - - attr_list - - md_in_html - -nav: - - Home: index.md - - Getting Started: getting-started.md - - Configuration: configuration.md - - Architecture: architecture.md - - Mathematical Model: mathematical-model.md - - Simulation Workflow: simulation-workflow.md - - Network Design: network-design.md - - Results: results.md - - References: - - Core: reference/core.md - - Models: reference/models.md - - Utils: reference/utils.md -extra: - social: - - icon: fontawesome/brands/github - link: https://github.com/dsih-artpark/patchsim \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 77cffb5..7dac095 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,9 +8,9 @@ version = "0.1.0b1" description = "A modular simulation framework for patch-based metapopulation epidemiology" readme = "README.md" requires-python = ">=3.11" -license = { text = "GPL-3.0" } +license = { file = "LICENSE" } classifiers = [ - "Development Status :: 3 - Alpha", + "Development Status :: 4 - Beta", "Intended Audience :: Science/Research", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Programming Language :: Python :: 3", @@ -28,33 +28,48 @@ authors = [ ] dependencies = [ - "numpy", - "pandas", - "scipy", - "matplotlib", - "pyyaml", - "tqdm", - "networkx", - "SALib", + "numpy>=1.26", + "pandas>=2.2", + "scipy>=1.11", + "matplotlib>=3.8", + "pyyaml>=6.0", + "tqdm>=4.66", + "networkx>=3.3", + "SALib>=1.5", "rich>=14.3.3", ] [project.urls] Homepage = "https://github.com/dsih-artpark/patchsim" Repository = "https://github.com/dsih-artpark/patchsim" -Documentation = "https://dsih-artpark.github.io/patchsim/" +Documentation = "https://dsih-artpark-patchsim.readthedocs.io/" [project.optional-dependencies] +test = [ + "pytest>=8.3.5", + "pytest-cov", +] + +docs = [ + "sphinx>=6.0", + "myst-parser>=0.18", + "pydata-sphinx-theme>=0.14", +] + +lint = [ + "ruff", + "mypy", + "pre-commit>=3.0.0", +] + dev = [ "pytest>=8.3.5", "pytest-cov", "ruff", "mypy", - "mkdocs>=1.6.1", - "mkdocs-material>=9.6.12", - "mkdocstrings[python]>=0.29.1", "rich>=13.0.0", - "pre-commit>=3.0.0" + "pre-commit>=3.0.0", + "twine>=5.0.0", ] notebook = [ @@ -78,9 +93,6 @@ fixable = ["ALL"] [tool.ruff.lint.isort] known-first-party = ["patchsim"] -[tool.ruff.lint.per-file-ignores] -"src/patchsim/models/ka_fmd_sirsv_discrete.py" = ["E501", "E741"] - [tool.hatch.build.targets.wheel] packages = ["src/patchsim"] include = [ diff --git a/src/patchsim/cli.py b/src/patchsim/cli.py index 784802b..8ba1268 100644 --- a/src/patchsim/cli.py +++ b/src/patchsim/cli.py @@ -1,16 +1,32 @@ import argparse +import json import logging import shutil import textwrap from importlib import resources from pathlib import Path +from typing import Any -from patchsim import __version__ -from patchsim.core.simulation import load_config, run_simulation, setup_simulation - +import yaml -def _configure_logging() -> None: +from patchsim import __version__ +from patchsim.core.simulation import ( + get_available_template_names, + get_config_schema, + get_init_template_config, + get_model_catalog, + load_config, + run_simulation, + setup_simulation, +) + + +def _configure_logging(*, json_output: bool = False) -> None: """Configure CLI logging with rich handler when available.""" + if json_output: + logging.basicConfig(level=logging.WARNING) + return + try: from rich.logging import RichHandler @@ -24,18 +40,35 @@ def _configure_logging() -> None: logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(name)s:%(message)s") -def _cmd_run(config_path: str) -> None: +def _emit_json(payload: dict[str, Any]) -> None: + print(json.dumps(payload, indent=2, sort_keys=True)) + + +def _cmd_run(config_path: str, *, json_output: bool = False) -> dict[str, Any]: logging.info("Starting PatchSim simulation...") config = load_config(config_path) net, y0, patches, num_patches = setup_simulation(config) - run_simulation(config, config["ModelName"], net, y0, patches, num_patches) + summary = run_simulation(config, config["ModelName"], net, y0, patches, num_patches) logging.info("Simulation completed successfully.") + return {"ok": True, "config": config_path, **summary} if json_output else summary -def _cmd_validate(config_path: str) -> None: +def _cmd_validate(config_path: str, *, json_output: bool = False, schema: bool = False) -> dict[str, Any] | None: + if schema: + return get_config_schema() + config = load_config(config_path) - setup_simulation(config) + _net, _y0, patches, num_patches = setup_simulation(config) logging.info("Configuration is valid: %s", config_path) + if json_output: + return { + "ok": True, + "config": config_path, + "model_name": config.get("ModelName"), + "num_patches": num_patches, + "patches": patches, + } + return None def _copy_template_tree(template_node, target_path: Path) -> None: @@ -49,7 +82,12 @@ def _copy_template_tree(template_node, target_path: Path) -> None: target_path.write_bytes(template_node.read_bytes()) -def _cmd_init(name: str, force: bool = False) -> None: +def _render_template_config(project_name: str, template_name: str) -> str: + config = get_init_template_config(template_name, project_name) + return yaml.safe_dump(config, sort_keys=False) + + +def _cmd_init(name: str, force: bool = False, template: str = "sir") -> None: project_dir = Path(name) # Safety checks: prevent deleting cwd, parent dirs, root, or non-directories @@ -77,48 +115,47 @@ def _cmd_init(name: str, force: bool = False) -> None: _copy_template_tree(template_root, project_dir) config_path = project_dir / "config.yaml" - rendered = config_path.read_text(encoding="utf-8").replace("{{PROJECT_NAME}}", project_dir.name) - config_path.write_text(rendered, encoding="utf-8") + config_path.write_text(_render_template_config(project_dir.name, template), encoding="utf-8") logging.info("Created project scaffold at: %s", project_dir) -def _list_builtin_models() -> list[str]: - models_dir = Path(__file__).resolve().parent / "models" - models = [] - for f in sorted(models_dir.glob("*.py")): - if f.name == "__init__.py": - continue - models.append(f.stem) - return models +def _list_builtin_models() -> list[dict[str, str]]: + return get_model_catalog() -def _cmd_list_models() -> None: +def _cmd_list_models(*, json_output: bool = False) -> list[dict[str, str]]: models = _list_builtin_models() if not models: logging.info("No built-in models found.") - return - logging.info("Built-in models:") + return [] + + if json_output: + return models + + logging.info("Built-in models and templates:") for model in models: - logging.info("- %s", model) + logging.info("- %s (%s)", model["name"], model["kind"]) + return models -def main(): +def main() -> None: """Command-line interface for running the PatchSim simulation.""" - _configure_logging() - parser = argparse.ArgumentParser( description=( "PatchSim: A modular metapopulation simulation framework for multi-disease epidemiological modelling." ), formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=textwrap.dedent(""" + epilog=textwrap.dedent( + """ Examples: uv run patchsim init my-project + uv run patchsim init my-project --template seir uv run patchsim run -c my-project/config.yaml uv run patchsim validate -c my-project/config.yaml uv run patchsim list-models - """), + """ + ), ) parser.add_argument("--version", action="version", version=f"patchsim {__version__}") @@ -127,26 +164,46 @@ def main(): init_p = subparsers.add_parser("init", help="Scaffold a new PatchSim project") init_p.add_argument("name", help="Directory name for the new project") init_p.add_argument("--force", action="store_true", help="Overwrite target directory if it already exists") + init_p.add_argument( + "--template", + choices=get_available_template_names(), + default="sir", + help="Starter template to use for config.yaml", + ) run_p = subparsers.add_parser("run", help="Run a simulation") run_p.add_argument("-c", "--config", required=True, help="Path to simulation config YAML") + run_p.add_argument("--json", action="store_true", help="Emit machine-readable JSON summary") validate_p = subparsers.add_parser("validate", help="Validate config and inputs") - validate_p.add_argument("-c", "--config", required=True, help="Path to simulation config YAML") + validate_p.add_argument("-c", "--config", help="Path to simulation config YAML") + validate_p.add_argument("--schema", action="store_true", help="Print the configuration JSON Schema") + validate_p.add_argument("--json", action="store_true", help="Emit machine-readable JSON summary") - subparsers.add_parser("list-models", help="List available built-in models") + list_p = subparsers.add_parser("list-models", help="List available built-in models") + list_p.add_argument("--json", action="store_true", help="Emit machine-readable JSON list") args = parser.parse_args() + json_mode = bool(getattr(args, "json", False) or getattr(args, "schema", False)) + _configure_logging(json_output=json_mode) try: if args.command == "init": - _cmd_init(args.name, force=args.force) + _cmd_init(args.name, force=args.force, template=args.template) elif args.command == "run": - _cmd_run(args.config) + result = _cmd_run(args.config, json_output=args.json) + if args.json: + _emit_json(result) elif args.command == "validate": - _cmd_validate(args.config) + if not args.schema and not args.config: + parser.error("the following arguments are required: -c/--config") + result = _cmd_validate(args.config, json_output=args.json, schema=args.schema) + if result is not None: + _emit_json(result) elif args.command == "list-models": - _cmd_list_models() + result = _cmd_list_models(json_output=args.json) + if args.json: + _emit_json({"models": result}) else: parser.print_help() raise SystemExit(2) diff --git a/src/patchsim/core/simulation.py b/src/patchsim/core/simulation.py index 7c3552a..56eb725 100644 --- a/src/patchsim/core/simulation.py +++ b/src/patchsim/core/simulation.py @@ -4,6 +4,8 @@ """ import os +import re +from pathlib import Path from typing import Any import numpy as np @@ -17,16 +19,159 @@ EPSILON = 1e-6 +MODEL_TEMPLATE_CONFIGS: dict[str, dict[str, Any]] = { + "sir": { + "compartments": ["S", "I", "R"], + "Parameters": {"beta": 0.08, "gamma": 0.1}, + "Transitions": {"S -> I": "beta", "I -> R": "gamma * I"}, + }, + "seir": { + "compartments": ["S", "E", "I", "R"], + "Parameters": {"beta": 0.08, "sigma": 0.2, "gamma": 0.1}, + "Transitions": {"S -> E": "beta", "E -> I": "sigma * E", "I -> R": "gamma * I"}, + }, + "sirs": { + "compartments": ["S", "I", "R"], + "Parameters": {"beta": 0.08, "gamma": 0.1, "waning": 0.02}, + "Transitions": {"S -> I": "beta", "I -> R": "gamma * I", "R -> S": "waning * R"}, + }, + "sis": { + "compartments": ["S", "I"], + "Parameters": {"beta": 0.08, "gamma": 0.1}, + "Transitions": {"S -> I": "beta", "I -> S": "gamma * I"}, + }, +} + + +def get_config_schema() -> dict[str, Any]: + """Return the JSON Schema for PatchSim configuration files.""" + return { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://dsih-artpark.github.io/patchsim/config.schema.json", + "title": "PatchSim configuration", + "type": "object", + "required": ["PatchFile", "SeedFile", "OutputDir", "Transitions", "TMax"], + "properties": { + "PatchFile": {"type": "string"}, + "SeedFile": {"type": "string"}, + "NetworkFile": {"type": ["string", "null"]}, + "OutputDir": {"type": "string"}, + "ModelName": {"type": "string"}, + "TMax": {"type": "integer", "minimum": 1}, + "Tolerance": {"type": ["number", "string"]}, + "MaxIter": {"type": "integer", "minimum": 1}, + "StartDate": {"type": ["string", "null"]}, + "EndDate": {"type": ["string", "null"]}, + "Logging": {"type": ["boolean", "string"]}, + "compartments": { + "type": "array", + "items": {"type": "string"}, + "minItems": 1, + }, + "Compartments": { + "type": "array", + "items": {"type": "string"}, + "minItems": 1, + }, + "Parameters": { + "type": "object", + "additionalProperties": {"type": ["number", "integer", "string", "boolean"]}, + }, + "PatchParameters": { + "type": "array", + "items": { + "type": "object", + "required": ["patch"], + "properties": { + "patch": {"type": "string"}, + "parameters": {"type": "object"}, + }, + "additionalProperties": True, + }, + }, + "Transitions": { + "type": "object", + "minProperties": 1, + "additionalProperties": {"type": "string"}, + }, + }, + "additionalProperties": True, + } + + +def get_model_catalog() -> list[dict[str, str]]: + """Return built-in model references and YAML templates for the CLI.""" + catalog: list[dict[str, str]] = [ + {"name": "ka_fmd_sirsv_discrete", "kind": "python"}, + ] + for template_name in sorted(MODEL_TEMPLATE_CONFIGS): + catalog.append({"name": template_name, "kind": "yaml-template"}) + return catalog + + +def get_available_template_names() -> list[str]: + """Return the built-in starter template names.""" + return sorted(MODEL_TEMPLATE_CONFIGS) + + +def get_init_template_config(template_name: str, project_name: str) -> dict[str, Any]: + """Return a starter project config for the requested template.""" + try: + template = MODEL_TEMPLATE_CONFIGS[template_name] + except KeyError as e: + raise ValueError( + f"Unknown template '{template_name}'. Available templates: {sorted(MODEL_TEMPLATE_CONFIGS)}" + ) from e + + config: dict[str, Any] = { + "PatchFile": "data/patch/patch-population.csv", + "NetworkFile": "data/networks/network-static.csv", + "SeedFile": "data/seeds/seed-initial.csv", + "Logging": False, + "ModelName": project_name, + "TMax": 60, + "Tolerance": 1e-8, + "MaxIter": 10000, + "StartDate": "2020-01-01", + "EndDate": "2022-12-31", + "OutputDir": f"output/{project_name}", + } + config.update(template) + return config + + def load_config(config_path: str) -> dict[str, Any]: """Load and validate configuration file.""" - with open(config_path) as f: + cfg_path = Path(config_path).expanduser().resolve() + with open(cfg_path, encoding="utf-8") as f: config = yaml.safe_load(f) + if not isinstance(config, dict): + raise ValueError( + f"Configuration error in {cfg_path}: expected YAML mapping (key-value pairs), " + "but got a list or scalar value instead. \n" + "Ensure the config file has the format: key1: value1\\n key2: value2" + ) + # Validate required fields required_fields = ["PatchFile", "SeedFile", "OutputDir", "Transitions", "TMax"] for field in required_fields: if field not in config: - raise ValueError(f"Missing required field '{field}' in config") + available = ", ".join(sorted(config.keys())) + raise ValueError( + f"Configuration error: missing required field '{field}'.\n" + f"Available fields in config: {available}\n" + f"Please add '{field}' to your config file." + ) + + # Resolve relative paths against the config file directory. + cfg_dir = cfg_path.parent + for key in ["PatchFile", "SeedFile", "NetworkFile", "OutputDir"]: + val = config.get(key) + if isinstance(val, str) and val.strip(): + p = Path(val).expanduser() + if not p.is_absolute(): + config[key] = str((cfg_dir / p).resolve()) return config @@ -40,21 +185,58 @@ def setup_simulation(config: dict[str, Any]) -> tuple[NetworkModel, dict[str, fl for p, pop in populations.items(): if pop <= 0: - raise ValueError(f"Population for patch {p} must be positive") + raise ValueError( + f"Invalid population in {config['PatchFile']}: patch '{p}' has population {pop}.\n" + "Population must be a positive number (> 0).\n" + "Please correct the population value in your patch file." + ) # Load seed data seed_df = pd.read_csv(config["SeedFile"]) - compartments = [col for col in seed_df.columns if col != "patch"] + seed_compartments = [col for col in seed_df.columns if col != "patch"] + + configured_compartments = config.get("compartments", config.get("Compartments")) + if configured_compartments is None: + compartments = seed_compartments + else: + if not isinstance(configured_compartments, list) or not configured_compartments: + raise ValueError("'compartments' must be a non-empty list when provided") + compartments = [str(c) for c in configured_compartments] + + missing_in_seed = sorted(set(compartments) - set(seed_compartments)) + extra_in_seed = sorted(set(seed_compartments) - set(compartments)) + if missing_in_seed or extra_in_seed: + raise ValueError( + f"Compartment mismatch between config and SeedFile ({config['SeedFile']}).\n" + f"Config compartments: {sorted(compartments)}\n" + f"SeedFile columns: {seed_compartments}\n" + f"Missing in SeedFile: {missing_in_seed}\n" + f"Extra in SeedFile: {extra_in_seed}\n" + "Please ensure the SeedFile has columns matching your config compartments." + ) for _, row in seed_df.iterrows(): patch = row["patch"] if patch not in populations: - raise ValueError(f"SeedFile contains unknown patch '{patch}' not present in PatchFile") + raise ValueError( + f"Unknown patch '{patch}' in SeedFile ({config['SeedFile']}).\n" + f"Known patches from PatchFile: {sorted(populations.keys())}\n" + "Please ensure all patches in SeedFile match PatchFile." + ) total = sum(row[c] for c in compartments) if not all(row[c] >= 0 for c in compartments): - raise ValueError(f"Seed values must be non-negative for patch {patch}") + neg_comps = [c for c in compartments if row[c] < 0] + raise ValueError( + f"Invalid seed data for patch '{patch}': negative values found.\n" + f"Compartments with negative values: {neg_comps}\n" + "All seed values must be non-negative." + ) if abs(total - populations[patch]) >= EPSILON: - raise ValueError(f"Seed values do not sum to population for patch {patch}") + raise ValueError( + f"Seed mismatch for patch '{patch}': seed sum ({total}) != population ({populations[patch]}).\n" + f"Seed compartments: {dict((c, row[c]) for c in compartments)}\n" + "Ensure seed values sum exactly to the patch population." + ) # Set up network num_patches = len(patches) @@ -72,17 +254,37 @@ def setup_simulation(config: dict[str, Any]) -> tuple[NetworkModel, dict[str, fl source = row["source"].strip('"') target = row["target"].strip('"') if source not in patch_idx: - raise ValueError(f"NetworkFile contains unknown source patch '{source}'") + raise ValueError( + f"Unknown source patch '{source}' in NetworkFile ({config['NetworkFile']}).\n" + f"Known patches: {sorted(patch_idx.keys())}\n" + "Please ensure all patches in NetworkFile match PatchFile." + ) if target not in patch_idx: - raise ValueError(f"NetworkFile contains unknown target patch '{target}'") + raise ValueError( + f"Unknown target patch '{target}' in NetworkFile ({config['NetworkFile']}).\n" + f"Known patches: {sorted(patch_idx.keys())}\n" + "Please ensure all patches in NetworkFile match PatchFile." + ) i = patch_idx[source] j = patch_idx[target] if row["weight"] < 0: - raise ValueError(f"Network weight must be non-negative between {row['source']} and {row['target']}") + raise ValueError( + f"Invalid network weight in NetworkFile ({config['NetworkFile']}): " + f"weight={row['weight']} from '{source}' to '{target}'.\n" + "Network weights must be non-negative. Please correct your network file." + ) network_matrix[i, j] = row["weight"] # Set up model global_params = config.get("Parameters", {}) + + # Collect per-patch parameters if provided (needed for transition-name validation too) + patch_params: dict[str, dict[str, Any]] = {} + if "PatchParameters" in config: + for entry in config["PatchParameters"]: + patch_name = entry["patch"] + patch_params[patch_name] = entry.get("parameters", {}) + transitions_cfg = config.get("Transitions", {}) # Transitions must be provided as arrow-map syntax in config, e.g.: # Transitions: {S -> I: beta, I -> R: gamma * I} @@ -90,18 +292,40 @@ def setup_simulation(config: dict[str, Any]) -> tuple[NetworkModel, dict[str, fl raise ValueError("'Transitions' must be a non-empty mapping in arrow syntax, e.g. {S -> I: 'beta'}.") transitions: list[dict[str, Any]] = [] + patch_param_names = set() + for per_patch in patch_params.values(): + patch_param_names |= set(per_patch.keys()) + allowed_names = set(compartments) | set(global_params.keys()) | patch_param_names for k, v in transitions_cfg.items(): parts = [p.strip() for p in str(k).split("->")] if len(parts) != 2 or not all(parts): - raise ValueError(f"Invalid transition key '{k}'. Use 'S -> I' format.") - transitions.append({"transition": f"{parts[0]}->{parts[1]}", "rate": v}) - - # Collect per-patch parameters if provided - patch_params: dict[str, dict[str, Any]] = {} - if "PatchParameters" in config: - for entry in config["PatchParameters"]: - patch_name = entry["patch"] - patch_params[patch_name] = entry.get("parameters", {}) + raise ValueError( + f"Invalid transition key '{k}'.\n" + "Use arrow format: 'source -> target' (e.g., 'S -> I')\n" + "Available compartments: {}".format(sorted(compartments)) + ) + source, target = parts[0], parts[1] + if source not in compartments or target not in compartments: + bad_comps = [p for p in [source, target] if p not in compartments] + raise ValueError( + f"Transition '{k}' uses unknown compartments: {bad_comps}\n" + f"Known compartments: {sorted(compartments)}\n" + "Please correct the transition definition." + ) + + if isinstance(v, str): + identifiers = set(re.findall(r"[A-Za-z_]\w*", v)) + python_keywords = {"and", "or", "not", "True", "False", "None"} + unknown_identifiers = sorted(identifiers - allowed_names - python_keywords) + if unknown_identifiers: + raise ValueError( + f"Transition '{k}' uses undefined names: {unknown_identifiers}\n" + f"Expression: '{v}'\n" + f"Defined names (compartments + parameters): {sorted(allowed_names)}\n" + "Please check your transition expression for typos or add missing parameters." + ) + + transitions.append({"transition": f"{source}->{target}", "rate": v}) # Validate that all configured patches exist in PatchFile unknown_patches = set(patch_params) - set(patches) @@ -134,8 +358,12 @@ def setup_simulation(config: dict[str, Any]) -> tuple[NetworkModel, dict[str, fl def run_simulation( config: dict[str, Any], model_name: str, net: NetworkModel, y0: dict[str, float], patches: list, num_patches: int -) -> None: - """Run the simulation and save results.""" +) -> dict[str, Any]: + """Run the simulation and save results. + + Returns: + A summary dictionary describing the generated artifacts. + """ # Create output directories for subdir in ["plots", "runs"]: dir_path = os.path.join(config["OutputDir"], subdir) @@ -150,7 +378,11 @@ def run_simulation( # Validate and construct time range t_max = config.get("TMax") if not isinstance(t_max, int) or t_max <= 0: - raise ValueError("'TMax' must be a positive integer.") + raise ValueError( + f"Invalid 'TMax' value: {t_max}\n" + "'TMax' must be a positive integer (number of time steps).\n" + "Example: TMax: 100" + ) t_range = np.arange(t_max, dtype=float) # Run simulation @@ -169,4 +401,15 @@ def run_simulation( model.visualize(t_range, out_ode, patches, plots_dir, model_name) - logger.info(f"Saved all patch subplots to {plots_dir}/patch_timeseries_{model_name}_ode.png") + plot_path = os.path.join(plots_dir, f"patch_timeseries_{model_name}_ode.png") + logger.info(f"Saved all patch subplots to {plot_path}") + + return { + "model_name": model_name, + "output_dir": config["OutputDir"], + "csv_path": csv_path, + "plot_path": plot_path, + "num_patches": num_patches, + "patches": patches, + "t_max": t_max, + } diff --git a/src/patchsim/models/ka_fmd_sirsv_discrete.py b/src/patchsim/models/ka_fmd_sirsv_discrete.py deleted file mode 100644 index 341cc6b..0000000 --- a/src/patchsim/models/ka_fmd_sirsv_discrete.py +++ /dev/null @@ -1,330 +0,0 @@ -import logging -import random -import warnings - -import numpy as np -from tqdm import tqdm - -warnings.filterwarnings("ignore") - - -def sirsv_model_with_weibull_random_vaccination(params, scenario, random_seed=42, diagnosis=None): - np.random.seed(random_seed) - random.seed(random_seed) - - # Extract parameters - beta = params["beta"] - gamma = params["gamma"] - vax_rate = params["vax_rate"] - weibull_shape_vax = params["weibull_shape_vax"] - weibull_scale_vax = params["weibull_scale_vax"] - weibull_shape_rec = params["weibull_shape_rec"] - weibull_scale_rec = params["weibull_scale_rec"] - days = params["days"] - seed_rate = params["seed_rate"] - vax_period = params["vax_period"] - vax_duration = params["vax_duration"] - start_vax_day = params["start_vax_day"] - - # Initial conditions - S0, I0, R0, V0 = params["S0"], params["I0"], params["R0"], params["V0"] - N = S0 + I0 + R0 + V0 - - S, I, R, V = [np.zeros(days) for _ in range(4)] - S[0], I[0], R[0], V[0] = S0, I0, R0, V0 - - decay_times_vax = [] - decay_times_rec = [] - - # Seed initial vaccinated and recovered individuals' waning times - if V0 > 0: - decay_times_vax.extend((weibull_scale_vax * np.random.weibull(weibull_shape_vax, int(V0))).astype(int).tolist()) - if R0 > 0: - decay_times_rec.extend((weibull_scale_rec * np.random.weibull(weibull_shape_rec, int(R0))).astype(int).tolist()) - - round_counter = 0 - - logging.info(f"Starting simulation for scenario: {scenario}") - - for t in tqdm(range(1, days), desc=f"Running {scenario} simulation", unit="day"): - new_seeds = min(seed_rate, S[t - 1]) - - # VACCINATION ROUND - if t == start_vax_day or (t > start_vax_day and (t - start_vax_day) % vax_period == 0): - round_counter += 1 - to_vaccinate = min(vax_rate * S[t - 1], S[t - 1]) - logging.info(f"Round {round_counter} start day: {t}") - - # Calculate the number of vaccinations to reset, considering the vaccination period - num_vax_to_reset = int(min(vax_rate * vax_period * V[t - 1], V[t - 1])) - if num_vax_to_reset > 0 and len(decay_times_vax) > 0: - num_vax_to_reset = min(num_vax_to_reset, len(decay_times_vax)) - - # Method 1: Randomly select decay times to reset - indices_to_reset = random.sample(range(len(decay_times_vax)), num_vax_to_reset) - - # Method 2: Sort decay times and select indices with the lowest decay times - # sorted_indices = np.argsort(decay_times_vax) - # indices_to_reset = sorted_indices[:num_vax_to_reset] - - for i in indices_to_reset: - decay_times_vax[i] = weibull_scale_vax * np.random.weibull(weibull_shape_vax) - logging.info(f"Day {t}: Re-vaccination reset: {num_vax_to_reset} decay times reset") - - # if diagnosis: - # plot_histogram(decay_times_vax, decay_times_rec, scenario, round_counter, start=True) - - # Check if it's within a vaccination period - is_vax_period = (t >= start_vax_day) and ((t - start_vax_day) % vax_period < vax_duration) - if is_vax_period: - new_vaccinations = int(to_vaccinate) - logging.info(f"Day {t}: Daily vaccinations: {new_vaccinations}") - - # Update compartments for new vaccinations - if new_vaccinations > 0: - new_susceptible_decay_times = ( - (weibull_scale_vax * np.random.weibull(weibull_shape_vax, new_vaccinations)).astype(int).tolist() - ) - initial_len = len(decay_times_vax) - decay_times_vax.extend(new_susceptible_decay_times) - updated_len = len(decay_times_vax) - logging.info( - f"Day {t}: New vaccinations: {new_vaccinations}, Length of decay_times_vax: {initial_len}, Length of decay_times_vax: {updated_len}" - ) - else: - S[t] = S[t - 1] - R[t] = R[t - 1] - V[t] = V[t - 1] - - else: - new_vaccinations = 0 - - # Calculate transitions - new_infections = beta * S[t - 1] * I[t - 1] / N + new_seeds - new_recoveries = gamma * I[t - 1] - - # Update compartments - S[t] = S[t - 1] - new_infections - new_vaccinations - I[t] = I[t - 1] + new_infections - new_recoveries - R[t] = R[t - 1] + new_recoveries - V[t] = V[t - 1] + new_vaccinations - - if new_recoveries > 0: - new_recovered_decay_times = ( - (weibull_scale_rec * np.random.weibull(weibull_shape_rec, int(new_recoveries))).astype(int).tolist() - ) - decay_times_rec.extend(new_recovered_decay_times) - - # IMMUNITY WANING - logging.info( - f"Day {t}: Before waning: Length of decay_times_vax={len(decay_times_vax)}, Length of decay_times_rec={len(decay_times_rec)}" - ) - - decay_times_vax_before = len(decay_times_vax) - decay_times_vax = [x - 1 for x in decay_times_vax if x > 0] - num_waned_vax = decay_times_vax_before - len(decay_times_vax) - - decay_times_rec_before = len(decay_times_rec) - decay_times_rec = [x - 1 for x in decay_times_rec if x > 0] - num_waned_rec = decay_times_rec_before - len(decay_times_rec) - - logging.info(f"Day {t}: Waned vaccinated: {num_waned_vax}, Waned recovered: {num_waned_rec}") - logging.info( - f"Day {t}: After waning: Length of decay_times_vax={len(decay_times_vax)}, Length of decay_times_rec={len(decay_times_rec)}" - ) - - # Move waned individuals back to susceptible compartment - S[t] += num_waned_vax + num_waned_rec - V[t] -= num_waned_vax - R[t] -= num_waned_rec - - # DIAGNOSIS AND LOG - logging.info( - f"Day {t}: Length of decay_times_vax={len(decay_times_vax)}, V[{t}]={V[t]}, Difference={V[t] - len(decay_times_vax)}" - ) - logging.info(f"Day {t}: S[t]={S[t]}, I[t]={I[t]}, R[t]={R[t]}, V[t]={V[t]}, Waned_vax={num_waned_vax}") - - if len(decay_times_vax) != V[t]: - logging.warning( - f"Day {t}: Length discrepancy: Length of decay_times_vax={len(decay_times_vax)}, V[t]={V[t]}" - ) - - total_population = S[t] + I[t] + R[t] + V[t] - if not np.isclose(total_population, N): - logging.error(f"Population not conserved on day {t}: Total={total_population}, Expected={N}") - - if S[t] < 0 or I[t] < 0 or R[t] < 0 or V[t] < 0: - logging.error(f"Negative compartment values on day {t}: S={S[t]}, I={I[t]}, R={R[t]}, V={V[t]}") - - # if is_vax_period and ((t - start_vax_day) % vax_period == vax_duration - 1) and diagnosis: - # plot_histogram(decay_times_vax, decay_times_rec, scenario, round_counter, start=False) - - if t % 30 == 0 or is_vax_period: - logging.info( - f"Day {t}: S={S[t]:.2f}, I={I[t]:.2f}, R={R[t]:.2f}, V={V[t]:.2f}, New Vaccinations={new_vaccinations if is_vax_period else 0}" - ) - - logging.info(f"Simulation of the {scenario.capitalize()} model completed.") - - return S, I, R, V - - -def sirsv_model_with_weibull_targetted_vaccination(params, scenario, random_seed=42, diagnosis=None): - np.random.seed(random_seed) - random.seed(random_seed) - - # Extract parameters - beta = params["beta"] - gamma = params["gamma"] - vax_rate = params["vax_rate"] - weibull_shape_vax = params["weibull_shape_vax"] - weibull_scale_vax = params["weibull_scale_vax"] - weibull_shape_rec = params["weibull_shape_rec"] - weibull_scale_rec = params["weibull_scale_rec"] - days = params["days"] - seed_rate = params["seed_rate"] - vax_period = params["vax_period"] - vax_duration = params["vax_duration"] - start_vax_day = params["start_vax_day"] - - # Initial conditions - S0, I0, R0, V0 = params["S0"], params["I0"], params["R0"], params["V0"] - N = S0 + I0 + R0 + V0 - - S, I, R, V = [np.zeros(days) for _ in range(4)] - S[0], I[0], R[0], V[0] = S0, I0, R0, V0 - - decay_times_vax = [] - decay_times_rec = [] - - # Seed initial vaccinated and recovered individuals' waning times - if V0 > 0: - decay_times_vax.extend((weibull_scale_vax * np.random.weibull(weibull_shape_vax, int(V0))).astype(int).tolist()) - if R0 > 0: - decay_times_rec.extend((weibull_scale_rec * np.random.weibull(weibull_shape_rec, int(R0))).astype(int).tolist()) - - round_counter = 0 - - logging.info(f"Starting simulation for scenario: {scenario}") - - for t in tqdm(range(1, days), desc=f"Running {scenario} simulation", unit="day"): - new_seeds = min(seed_rate, S[t - 1]) - - # VACCINATION ROUND - if t == start_vax_day or (t > start_vax_day and (t - start_vax_day) % vax_period == 0): - round_counter += 1 - to_vaccinate = min(vax_rate * S[t - 1], S[t - 1]) - logging.info(f"Round {round_counter} start day: {t}") - - # Calculate the number of vaccinations to reset, considering the vaccination period - num_vax_to_reset = int(min(vax_rate * vax_period * V[t - 1], V[t - 1])) - if num_vax_to_reset > 0 and len(decay_times_vax) > 0: - num_vax_to_reset = min(num_vax_to_reset, len(decay_times_vax)) - - # Method 1: Randomly select decay times to reset - # indices_to_reset = random.sample(range(len(decay_times_vax)), num_vax_to_reset) - - # Method 2: Sort decay times and select indices with the lowest decay times - sorted_indices = np.argsort(decay_times_vax) - indices_to_reset = sorted_indices[:num_vax_to_reset] - - for i in indices_to_reset: - decay_times_vax[i] = weibull_scale_vax * np.random.weibull(weibull_shape_vax) - logging.info(f"Day {t}: Re-vaccination reset: {num_vax_to_reset} decay times reset") - - # if diagnosis: - # plot_histogram(decay_times_vax, decay_times_rec, scenario, round_counter, start=True) - - # Check if it's within a vaccination period - is_vax_period = (t >= start_vax_day) and ((t - start_vax_day) % vax_period < vax_duration) - if is_vax_period: - new_vaccinations = int(to_vaccinate) - logging.info(f"Day {t}: Daily vaccinations: {new_vaccinations}") - - # Update compartments for new vaccinations - if new_vaccinations > 0: - new_susceptible_decay_times = ( - (weibull_scale_vax * np.random.weibull(weibull_shape_vax, new_vaccinations)).astype(int).tolist() - ) - initial_len = len(decay_times_vax) - decay_times_vax.extend(new_susceptible_decay_times) - updated_len = len(decay_times_vax) - logging.info( - f"Day {t}: New vaccinations: {new_vaccinations}, Length of decay_times_vax: {initial_len}, Length of decay_times_vax: {updated_len}" - ) - else: - S[t] = S[t - 1] - R[t] = R[t - 1] - V[t] = V[t - 1] - - else: - new_vaccinations = 0 - - # Calculate transitions - new_infections = beta * S[t - 1] * I[t - 1] / N + new_seeds - new_recoveries = gamma * I[t - 1] - - # Update compartments - S[t] = S[t - 1] - new_infections - new_vaccinations - I[t] = I[t - 1] + new_infections - new_recoveries - R[t] = R[t - 1] + new_recoveries - V[t] = V[t - 1] + new_vaccinations - - if new_recoveries > 0: - new_recovered_decay_times = ( - (weibull_scale_rec * np.random.weibull(weibull_shape_rec, int(new_recoveries))).astype(int).tolist() - ) - decay_times_rec.extend(new_recovered_decay_times) - - # IMMUNITY WANING - logging.info( - f"Day {t}: Before waning: Length of decay_times_vax={len(decay_times_vax)}, Length of decay_times_rec={len(decay_times_rec)}" - ) - - decay_times_vax_before = len(decay_times_vax) - decay_times_vax = [x - 1 for x in decay_times_vax if x > 0] - num_waned_vax = decay_times_vax_before - len(decay_times_vax) - - decay_times_rec_before = len(decay_times_rec) - decay_times_rec = [x - 1 for x in decay_times_rec if x > 0] - num_waned_rec = decay_times_rec_before - len(decay_times_rec) - - logging.info(f"Day {t}: Waned vaccinated: {num_waned_vax}, Waned recovered: {num_waned_rec}") - logging.info( - f"Day {t}: After waning: Length of decay_times_vax={len(decay_times_vax)}, Length of decay_times_rec={len(decay_times_rec)}" - ) - - # Move waned individuals back to susceptible compartment - S[t] += num_waned_vax + num_waned_rec - V[t] -= num_waned_vax - R[t] -= num_waned_rec - - # DIAGNOSIS AND LOG - logging.info( - f"Day {t}: Length of decay_times_vax={len(decay_times_vax)}, V[{t}]={V[t]}, Difference={V[t] - len(decay_times_vax)}" - ) - logging.info(f"Day {t}: S[t]={S[t]}, I[t]={I[t]}, R[t]={R[t]}, V[t]={V[t]}, Waned_vax={num_waned_vax}") - - if len(decay_times_vax) != V[t]: - logging.warning( - f"Day {t}: Length discrepancy: Length of decay_times_vax={len(decay_times_vax)}, V[t]={V[t]}" - ) - - total_population = S[t] + I[t] + R[t] + V[t] - if not np.isclose(total_population, N): - logging.error(f"Population not conserved on day {t}: Total={total_population}, Expected={N}") - - if S[t] < 0 or I[t] < 0 or R[t] < 0 or V[t] < 0: - logging.error(f"Negative compartment values on day {t}: S={S[t]}, I={I[t]}, R={R[t]}, V={V[t]}") - - # if is_vax_period and ((t - start_vax_day) % vax_period == vax_duration - 1) and diagnosis: - # plot_histogram(decay_times_vax, decay_times_rec, scenario, round_counter, start=False) - - if t % 30 == 0 or is_vax_period: - logging.info( - f"Day {t}: S={S[t]:.2f}, I={I[t]:.2f}, R={R[t]:.2f}, V={V[t]:.2f}, New Vaccinations={new_vaccinations if is_vax_period else 0}" - ) - - logging.info(f"Simulation of the {scenario.capitalize()} model completed.") - - return S, I, R, V diff --git a/src/patchsim/templates/models/seir.yaml b/src/patchsim/templates/models/seir.yaml new file mode 100644 index 0000000..7d58096 --- /dev/null +++ b/src/patchsim/templates/models/seir.yaml @@ -0,0 +1,10 @@ +# Built-in SEIR template +compartments: ["S", "E", "I", "R"] +Parameters: + beta: 0.25 + sigma: 0.2 + gamma: 0.1 +Transitions: + "S -> E": "beta" + "E -> I": "sigma * E" + "I -> R": "gamma * I" diff --git a/src/patchsim/templates/models/sir.yaml b/src/patchsim/templates/models/sir.yaml new file mode 100644 index 0000000..47fd2d4 --- /dev/null +++ b/src/patchsim/templates/models/sir.yaml @@ -0,0 +1,8 @@ +# Built-in SIR template +compartments: ["S", "I", "R"] +Parameters: + beta: 0.2 + gamma: 0.1 +Transitions: + "S -> I": "beta" + "I -> R": "gamma * I" diff --git a/src/patchsim/templates/models/sirs.yaml b/src/patchsim/templates/models/sirs.yaml new file mode 100644 index 0000000..be7a997 --- /dev/null +++ b/src/patchsim/templates/models/sirs.yaml @@ -0,0 +1,10 @@ +# Built-in SIRS template +compartments: ["S", "I", "R"] +Parameters: + beta: 0.2 + gamma: 0.1 + waning: 0.02 +Transitions: + "S -> I": "beta" + "I -> R": "gamma * I" + "R -> S": "waning * R" diff --git a/src/patchsim/templates/models/sis.yaml b/src/patchsim/templates/models/sis.yaml new file mode 100644 index 0000000..14648b7 --- /dev/null +++ b/src/patchsim/templates/models/sis.yaml @@ -0,0 +1,8 @@ +# Built-in SIS template +compartments: ["S", "I"] +Parameters: + beta: 0.08 + gamma: 0.1 +Transitions: + "S -> I": "beta" + "I -> S": "gamma * I" diff --git a/src/patchsim/utils/loader.py b/src/patchsim/utils/loader.py index 7842151..d147e9a 100644 --- a/src/patchsim/utils/loader.py +++ b/src/patchsim/utils/loader.py @@ -1,106 +1,6 @@ -""" -Utility functions for loading and parsing data files. -""" - -import logging -import random -from datetime import datetime - -import numpy as np -import pandas as pd -import yaml - -logger = logging.getLogger("PatchSimLogger") - - -def read_config(config_path: str) -> dict[str, str]: - """Read and parse a YAML configuration file. - - Args: - config_path: Path to the YAML configuration file - - Returns: - Dictionary containing configuration parameters - """ - with open(config_path, "r") as f: - return yaml.safe_load(f) - - -def read_patch_population(file_path: str) -> list[dict[str, int]]: - """Read and parse patch population data from a CSV file. - - Args: - file_path: Path to the CSV file containing patch population data - - Returns: - List of dictionaries containing patch population data - """ - df = pd.read_csv(file_path) - return df.to_dict("records") - +"""Deprecated placeholder module. -def read_network(file_path: str) -> list[dict[str, float]]: - """Read and parse the network file for patch connectivity. - - Args: - file_path: Path to the CSV file containing network data - - Returns: - List of dictionaries containing network connections - """ - df = pd.read_csv(file_path) - return df.to_dict("records") - - -def read_seeding_infection( - file_path: str, - start_date: datetime, - end_date: datetime, -) -> list[dict[str, str]]: - """Read and parse the seeding infection data. - - Args: - file_path: Path to the CSV file containing seeding data - start_date: Start date of the simulation period - end_date: End date of the simulation period - - Returns: - List of dictionaries containing seeding data - """ - df = pd.read_csv(file_path) - df["date"] = pd.to_datetime(df["date"]) - mask = (df["date"] >= start_date) & (df["date"] <= end_date) - return df[mask].to_dict("records") - - -def apply_seeding_infections( - patches: list[dict[str, int]], - seeds: list[dict[str, str]], - current_date: datetime, -) -> list[dict[str, int]]: - """Apply seeding infections to patches based on the current date. - - Args: - patches: List of dictionaries containing patch data - seeds: List of dictionaries containing seeding data - current_date: Current simulation date - - Returns: - Updated list of patch data with seeding infections applied - """ - for seed in seeds: - if pd.to_datetime(seed["date"]) == current_date: - patch_idx = int(seed["patch"]) - 1 - patches[patch_idx]["infected"] += int(seed["count"]) - return patches - - -def set_random_seed(*, seed: int) -> None: - """Set the random seed for reproducibility. - - Args: - seed (int): Random seed value. - """ - np.random.seed(seed) - random.seed(seed) - logger.info(f"Random seed set to {seed}.") +Legacy loader helpers were removed because they were unused and did not reflect +the active simulation pipeline. Use `patchsim.core.simulation` and +`patchsim.utils.logger` / `patchsim.utils.viz` instead. +""" diff --git a/test.py b/test.py deleted file mode 100644 index 4265bfd..0000000 --- a/test.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Deprecated compatibility placeholder. - -This file exists for backward compatibility with older test runners that expect a test.py at the repository root. -It is no longer used by the current pytest setup (see tests/ directory for all tests). -Scheduled for removal: when Python < 3.10 support is dropped. -Refer to: pyproject.toml [project.optional-dependencies] for test dependencies. -""" diff --git a/tests/test_config.py b/tests/test_config.py index c8e1574..63b2c95 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,3 +1,5 @@ +from pathlib import Path + import pytest from patchsim.core.simulation import load_config @@ -8,7 +10,8 @@ def test_load_config_success(tmp_path): cfg_file.write_text('PatchFile: foo\nSeedFile: bar\nOutputDir: out\nTMax: 10\nTransitions:\n "S -> I": "beta"\n') cfg = load_config(str(cfg_file)) assert "PatchFile" in cfg - assert cfg["OutputDir"] == "out" + assert Path(cfg["OutputDir"]).is_absolute() + assert Path(cfg["OutputDir"]).name == "out" def test_load_config_missing_field(tmp_path): diff --git a/tests/test_simulation_setup.py b/tests/test_simulation_setup.py index 8bfa1f4..2906f88 100644 --- a/tests/test_simulation_setup.py +++ b/tests/test_simulation_setup.py @@ -1,3 +1,5 @@ +from pathlib import Path + import pandas as pd import pytest import yaml @@ -41,3 +43,64 @@ def test_setup_simulation_rejects_list_transition_format(tmp_data_dir): cfg = load_config(tmp_data_dir["config"]) with pytest.raises(ValueError): setup_simulation(cfg) + + +def test_load_config_resolves_paths_relative_to_config(tmp_path): + cfg_dir = tmp_path / "proj" + (cfg_dir / "data" / "patch").mkdir(parents=True) + (cfg_dir / "data" / "seeds").mkdir(parents=True) + (cfg_dir / "output").mkdir(parents=True) + + patch_csv = cfg_dir / "data" / "patch" / "patch.csv" + seed_csv = cfg_dir / "data" / "seeds" / "seed.csv" + + pd.DataFrame({"patch": ["A"], "Population": [100]}).to_csv(patch_csv, index=False) + pd.DataFrame({"patch": ["A"], "S": [99], "I": [1], "R": [0]}).to_csv(seed_csv, index=False) + + config_path = cfg_dir / "config.yaml" + with open(config_path, "w", encoding="utf-8") as f: + yaml.safe_dump( + { + "PatchFile": "data/patch/patch.csv", + "SeedFile": "data/seeds/seed.csv", + "OutputDir": "output", + "TMax": 5, + "compartments": ["S", "I", "R"], + "Parameters": {"beta": 0.2, "gamma": 0.1}, + "Transitions": {"S -> I": "beta", "I -> R": "gamma * I"}, + }, + f, + ) + + cfg = load_config(str(config_path)) + assert Path(cfg["PatchFile"]).is_absolute() + assert Path(cfg["SeedFile"]).is_absolute() + assert Path(cfg["OutputDir"]).is_absolute() + + +def test_setup_simulation_rejects_unknown_transition_identifiers(tmp_data_dir): + with open(tmp_data_dir["config"], "r", encoding="utf-8") as f: + cfg = yaml.safe_load(f) + + cfg["Transitions"] = {"S -> I": "betax", "I -> R": "gamma * I"} + + with open(tmp_data_dir["config"], "w", encoding="utf-8") as f: + yaml.safe_dump(cfg, f) + + cfg = load_config(tmp_data_dir["config"]) + with pytest.raises(ValueError, match="undefined names"): + setup_simulation(cfg) + + +def test_setup_simulation_rejects_compartment_mismatch(tmp_data_dir): + with open(tmp_data_dir["config"], "r", encoding="utf-8") as f: + cfg = yaml.safe_load(f) + + cfg["compartments"] = ["S", "I", "R", "E"] + + with open(tmp_data_dir["config"], "w", encoding="utf-8") as f: + yaml.safe_dump(cfg, f) + + cfg = load_config(tmp_data_dir["config"]) + with pytest.raises(ValueError, match="Compartment mismatch"): + setup_simulation(cfg) diff --git a/tests/test_smoke.py b/tests/test_smoke.py index 16995c3..b2f6a83 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -1,3 +1,4 @@ +import json import subprocess @@ -51,6 +52,44 @@ def test_cli_list_models(): assert result.returncode == 0 output = (result.stdout + result.stderr).lower() assert "ka_fmd_sirsv_discrete" in output + assert "sir (yaml-template)" in output + assert "sis (yaml-template)" in output + + +def test_cli_list_models_json(): + result = subprocess.run( + ["patchsim", "list-models", "--json"], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + payload = json.loads(result.stdout) + assert any(item["name"] == "sir" and item["kind"] == "yaml-template" for item in payload["models"]) + + +def test_cli_validate_schema(): + result = subprocess.run( + ["patchsim", "validate", "--schema"], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + schema = json.loads(result.stdout) + assert schema["title"] == "PatchSim configuration" + + +def test_cli_init_template_scaffold(tmp_path): + project_dir = tmp_path / "template-project" + result = subprocess.run( + ["patchsim", "init", str(project_dir), "--template", "seir"], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + config_text = (project_dir / "config.yaml").read_text(encoding="utf-8") + assert "E" in config_text + assert "S -> E" in config_text def test_cli_init_scaffold(tmp_path): diff --git a/uv.lock b/uv.lock index 90dbb32..24cadb1 100644 --- a/uv.lock +++ b/uv.lock @@ -6,6 +6,27 @@ resolution-markers = [ "python_full_version < '3.12'", ] +[[package]] +name = "accessible-pygments" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + [[package]] name = "anyio" version = "4.9.0" @@ -112,16 +133,12 @@ wheels = [ ] [[package]] -name = "backrefs" -version = "5.8" +name = "backports-tarfile" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/46/caba1eb32fa5784428ab401a5487f73db4104590ecd939ed9daaf18b47e0/backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd", size = 6773994, upload-time = "2025-02-25T18:15:32.003Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, ] [[package]] @@ -165,47 +182,72 @@ wheels = [ [[package]] name = "cffi" -version = "1.17.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, - { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, - { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, - { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, - { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, - { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, - { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, - { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, - { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, - { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, - { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, - { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, - { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] [[package]] @@ -265,18 +307,6 @@ wheels = [ { 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.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { 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 = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -411,6 +441,54 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[package]] +name = "cryptography" +version = "46.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, + { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, + { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" }, + { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -477,6 +555,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + [[package]] name = "executing" version = "2.2.0" @@ -546,30 +633,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, ] -[[package]] -name = "ghp-import" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, -] - -[[package]] -name = "griffe" -version = "1.7.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a9/3e/5aa9a61f7c3c47b0b52a1d930302992229d191bf4bc76447b324b731510a/griffe-1.7.3.tar.gz", hash = "sha256:52ee893c6a3a968b639ace8015bec9d36594961e156e23315c8e8e51401fa50b", size = 395137, upload-time = "2025-04-23T11:29:09.147Z" } -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" @@ -607,6 +670,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "id" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069", size = 18088, upload-time = "2026-02-04T16:19:41.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl", hash = "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca", size = 14689, upload-time = "2026-02-04T16:19:40.051Z" }, +] + [[package]] name = "identify" version = "2.6.17" @@ -625,6 +700,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] +[[package]] +name = "imagesize" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, +] + [[package]] name = "iniconfig" version = "2.1.0" @@ -720,6 +816,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, ] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, +] + [[package]] name = "jedi" version = "0.19.2" @@ -732,6 +864,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, ] +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -1001,6 +1142,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/6a/ca128561b22b60bd5a0c4ea26649e68c8556b82bc70a0c396eebc977fe86/jupyterlab_widgets-3.0.15-py3-none-any.whl", hash = "sha256:d59023d7d7ef71400d51e6fee9a88867f6e65e10a4201605d2d7f3e8f012a31c", size = 216571, upload-time = "2025-05-05T12:32:29.534Z" }, ] +[[package]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + [[package]] name = "kiwisolver" version = "1.4.8" @@ -1067,15 +1226,6 @@ wheels = [ { 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" }, ] -[[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" }, -] - [[package]] name = "markdown-it-py" version = "4.0.0" @@ -1192,21 +1342,24 @@ wheels = [ ] [[package]] -name = "mdurl" -version = "0.1.2" +name = "mdit-py-plugins" +version = "0.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, ] [[package]] -name = "mergedeep" -version = "1.3.4" +name = "mdurl" +version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] @@ -1219,122 +1372,12 @@ wheels = [ ] [[package]] -name = "mkdocs" -version = "1.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "ghp-import" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mergedeep" }, - { name = "mkdocs-get-deps" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "pyyaml" }, - { name = "pyyaml-env-tag" }, - { name = "watchdog" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, -] - -[[package]] -name = "mkdocs-autorefs" -version = "1.4.2" +name = "more-itertools" +version = "11.0.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mkdocs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/47/0c/c9826f35b99c67fa3a7cddfa094c1a6c43fafde558c309c6e4403e5b37dc/mkdocs_autorefs-1.4.2.tar.gz", hash = "sha256:e2ebe1abd2b67d597ed19378c0fff84d73d1dbce411fce7a7cc6f161888b6749", size = 54961, upload-time = "2025-05-20T13:09:09.886Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/dc/fc063b78f4b769d1956319351704e23ebeba1e9e1d6a41b4b602325fd7e4/mkdocs_autorefs-1.4.2-py3-none-any.whl", hash = "sha256:83d6d777b66ec3c372a1aad4ae0cf77c243ba5bcda5bf0c6b8a2c5e7a3d89f13", size = 24969, upload-time = "2025-05-20T13:09:08.237Z" }, -] - -[[package]] -name = "mkdocs-get-deps" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mergedeep" }, - { name = "platformdirs" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, -] - -[[package]] -name = "mkdocs-material" -version = "9.6.14" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "babel" }, - { name = "backrefs" }, - { name = "colorama" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "mkdocs" }, - { name = "mkdocs-material-extensions" }, - { name = "paginate" }, - { name = "pygments" }, - { name = "pymdown-extensions" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/fa/0101de32af88f87cf5cc23ad5f2e2030d00995f74e616306513431b8ab4b/mkdocs_material-9.6.14.tar.gz", hash = "sha256:39d795e90dce6b531387c255bd07e866e027828b7346d3eba5ac3de265053754", size = 3951707, upload-time = "2025-05-13T13:27:57.173Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/a1/7fdb959ad592e013c01558822fd3c22931a95a0f08cf0a7c36da13a5b2b5/mkdocs_material-9.6.14-py3-none-any.whl", hash = "sha256:3b9cee6d3688551bf7a8e8f41afda97a3c39a12f0325436d76c86706114b721b", size = 8703767, upload-time = "2025-05-13T13:27:54.089Z" }, -] - -[[package]] -name = "mkdocs-material-extensions" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, -] - -[[package]] -name = "mkdocstrings" -version = "0.29.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mkdocs" }, - { name = "mkdocs-autorefs" }, - { name = "pymdown-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/41/e8/d22922664a627a0d3d7ff4a6ca95800f5dde54f411982591b4621a76225d/mkdocstrings-0.29.1.tar.gz", hash = "sha256:8722f8f8c5cd75da56671e0a0c1bbed1df9946c0cef74794d6141b34011abd42", size = 1212686, upload-time = "2025-03-31T08:33:11.997Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/14/22533a578bf8b187e05d67e2c1721ce10e3f526610eebaf7a149d557ea7a/mkdocstrings-0.29.1-py3-none-any.whl", hash = "sha256:37a9736134934eea89cbd055a513d40a020d87dfcae9e3052c2a6b8cd4af09b6", size = 1631075, upload-time = "2025-03-31T08:33:09.661Z" }, -] - -[package.optional-dependencies] -python = [ - { name = "mkdocstrings-python" }, -] - -[[package]] -name = "mkdocstrings-python" -version = "1.16.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "griffe" }, - { name = "mkdocs-autorefs" }, - { name = "mkdocstrings" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bf/ed/b886f8c714fd7cccc39b79646b627dbea84cd95c46be43459ef46852caf0/mkdocstrings_python-1.16.12.tar.gz", hash = "sha256:9b9eaa066e0024342d433e332a41095c4e429937024945fea511afe58f63175d", size = 206065, upload-time = "2025-06-03T12:52:49.276Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/f7/139d22fef48ac78127d18e01d80cf1be40236ae489769d17f35c3d425293/more_itertools-11.0.2.tar.gz", hash = "sha256:392a9e1e362cbc106a2457d37cabf9b36e5e12efd4ebff1654630e76597df804", size = 144659, upload-time = "2026-04-09T15:01:33.297Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/dd/a24ee3de56954bfafb6ede7cd63c2413bb842cc48eb45e41c43a05a33074/mkdocstrings_python-1.16.12-py3-none-any.whl", hash = "sha256:22ded3a63b3d823d57457a70ff9860d5a4de9e8b1e482876fc9baabaf6f5f374", size = 124287, upload-time = "2025-06-03T12:52:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.2-py3-none-any.whl", hash = "sha256:6e35b35f818b01f691643c6c611bc0902f2e92b46c18fffa77ae1e7c46e912e4", size = 71939, upload-time = "2026-04-09T15:01:32.21Z" }, ] [[package]] @@ -1398,6 +1441,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "myst-parser" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "jinja2" }, + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/fa/7b45eef11b7971f0beb29d27b7bfe0d747d063aa29e170d9edd004733c8a/myst_parser-5.0.0.tar.gz", hash = "sha256:f6f231452c56e8baa662cc352c548158f6a16fcbd6e3800fc594978002b94f3a", size = 98535, upload-time = "2026-01-15T09:08:18.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/ac/686789b9145413f1a61878c407210e41bfdb097976864e0913078b24098c/myst_parser-5.0.0-py3-none-any.whl", hash = "sha256:ab31e516024918296e169139072b81592336f2fef55b8986aa31c9f04b5f7211", size = 84533, upload-time = "2026-01-15T09:08:16.788Z" }, +] + [[package]] name = "narwhals" version = "1.42.0" @@ -1480,6 +1541,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" }, ] +[[package]] +name = "nh3" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/86/f8d3a7c9bd1bbaa181f6312c757e0b74d25f71ecf84ea3c0dc5e0f01840d/nh3-0.3.4.tar.gz", hash = "sha256:96709a379997c1b28c8974146ca660b0dcd3794f4f6d50c1ea549bab39ac6ade", size = 19520, upload-time = "2026-03-25T10:57:30.789Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/5e/c400663d14be2216bc084ed2befc871b7b12563f85d40904f2a4bf0dd2b7/nh3-0.3.4-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8b61058f34c2105d44d2a4d4241bacf603a1ef5c143b08766bbd0cf23830118f", size = 1417991, upload-time = "2026-03-25T10:56:59.13Z" }, + { url = "https://files.pythonhosted.org/packages/36/f5/109526f5002ec41322ac8cafd50f0f154bae0c26b9607c0fcb708bdca8ec/nh3-0.3.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:554cc2bab281758e94d770c3fb0bf2d8be5fb403ef6b2e8841dd7c1615df7a0f", size = 790566, upload-time = "2026-03-25T10:57:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/7b/66/38950f2b4b316ffd82ee51ed8f9143d1f56fdd620312cacc91613b77b3e7/nh3-0.3.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dbe76feaa44e2ef9436f345016012a591550e77818876a8de5c8bc2a248e08df", size = 837538, upload-time = "2026-03-25T10:57:01.848Z" }, + { url = "https://files.pythonhosted.org/packages/d8/9f/9d6da970e9524fe360ea02a2082856390c2c8ba540409d1be6e5851887b3/nh3-0.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:87dac8d611b4a478400e0821a13b35770e88c266582f065e7249d6a37b0f86e8", size = 1012154, upload-time = "2026-03-25T10:57:03.592Z" }, + { url = "https://files.pythonhosted.org/packages/54/92/7c85c33c241e9dd51dda115bd3f765e940446588cdaaca62ef8edffe675f/nh3-0.3.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8d697e19f2995b337f648204848ac3a528eaafffc39e7ce4ac6b7a2fbe6c84af", size = 1092516, upload-time = "2026-03-25T10:57:04.726Z" }, + { url = "https://files.pythonhosted.org/packages/16/0f/597842bdb2890999a3faa2f3fcb02db8aa6ad09320d3d843ff6d0a1f737b/nh3-0.3.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:7cae217f031809321db962cd7e092bda8d4e95a87f78c0226628fa6c2ea8ebc5", size = 1053793, upload-time = "2026-03-25T10:57:06.171Z" }, + { url = "https://files.pythonhosted.org/packages/7d/32/669da65147bc10746d2e1d7a8a3dbfbffe0315f419e74b559e2ee3471a01/nh3-0.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:07999b998bf89692738f15c0eac76a416382932f855709e0b7488b595c30ec89", size = 1035975, upload-time = "2026-03-25T10:57:07.292Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/9e97a8b3c5161c79b4bf21cc54e9334860a52cc54ede15bf2239ef494b73/nh3-0.3.4-cp314-cp314t-win32.whl", hash = "sha256:ca90397c8d36c1535bf1988b2bed006597337843a164c7ec269dc8813f37536b", size = 600419, upload-time = "2026-03-25T10:57:08.342Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c7/6849d8d4295d3997d148eacb2d4b1c9faada4895ee3c1b1e12e72f4611e2/nh3-0.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:41e46b3499918ab6128b6421677b316e79869d0c140da24069d220a94f4e72d1", size = 613342, upload-time = "2026-03-25T10:57:09.593Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0e/14a3f510f36c20b922c123a2730f071f938d006fb513aacfd46d6cbc03a7/nh3-0.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:80b955d802bf365bd42e09f6c3d64567dce777d20e97968d94b3e9d9e99b265e", size = 607025, upload-time = "2026-03-25T10:57:10.959Z" }, + { url = "https://files.pythonhosted.org/packages/4a/57/a97955bc95960cfb1f0517043d60a121f4ba93fde252d4d9ffd3c2a9eead/nh3-0.3.4-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d8bebcb20ab4b91858385cd98fe58046ec4a624275b45ef9b976475604f45b49", size = 1439519, upload-time = "2026-03-25T10:57:12.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/c9a33361da8cde7c7760f091cd10467bc470634e4eea31c8bb70935b00a4/nh3-0.3.4-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d825722a1e8cbc87d7ca1e47ffb1d2a6cf343ad4c1b8465becf7cadcabcdfd0", size = 833798, upload-time = "2026-03-25T10:57:13.264Z" }, + { url = "https://files.pythonhosted.org/packages/6b/19/9487790780b8c94eacca37866c1270b747a4af8e244d43b3b550fddbbf62/nh3-0.3.4-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4aa8b43e68c26b68069a3b6cef09de166d1d7fa140cf8d77e409a46cbf742e44", size = 820414, upload-time = "2026-03-25T10:57:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b4/c6a340dd321d20b1e4a663307032741da045685c87403926c43656f6f5ec/nh3-0.3.4-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f5f214618ad5eff4f2a6b13a8d4da4d9e7f37c569d90a13fb9f0caaf7d04fe21", size = 1061531, upload-time = "2026-03-25T10:57:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/c4/49/f6b4b474e0032e4bcbb7174b44e4cf6915670e09c62421deb06ccfcb88b8/nh3-0.3.4-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3390e4333883673a684ce16c1716b481e91782d6f56dec5c85fed9feedb23382", size = 1021889, upload-time = "2026-03-25T10:57:16.454Z" }, + { url = "https://files.pythonhosted.org/packages/43/da/e52a6941746d1f974752af3fc8591f1dbcdcf7fd8c726c7d99f444ba820e/nh3-0.3.4-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18a2e44ccb29cbb45071b8f3f2dab9ebfb41a6516f328f91f1f1fd18196239a4", size = 912965, upload-time = "2026-03-25T10:57:17.624Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b7/ec1cbc6b297a808c513f59f501656389623fc09ad6a58c640851289c7854/nh3-0.3.4-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0961a27dc2057c38d0364cb05880e1997ae1c80220cbc847db63213720b8f304", size = 804975, upload-time = "2026-03-25T10:57:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/a9/56/b1275aa2c6510191eed76178da4626b0900402439cb9f27d6b9bf7c6d5e9/nh3-0.3.4-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:9337517edb7c10228252cce2898e20fb3d77e32ffaccbb3c66897927d74215a0", size = 833400, upload-time = "2026-03-25T10:57:20.086Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a5/5d574ffa3c6e49a5364d1b25ebad165501c055340056671493beb467a15e/nh3-0.3.4-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d866701affe67a5171b916b5c076e767a74c6a9efb7fb2006eb8d3c5f9a293d5", size = 854277, upload-time = "2026-03-25T10:57:21.433Z" }, + { url = "https://files.pythonhosted.org/packages/79/36/8aeb2ab21517cefa212db109e41024e02650716cb42bf293d0a88437a92d/nh3-0.3.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:47d749d99ae005ab19517224140b280dd56e77b33afb82f9b600e106d0458003", size = 1022021, upload-time = "2026-03-25T10:57:22.433Z" }, + { url = "https://files.pythonhosted.org/packages/9c/95/9fd860997685e64abe2d5a995ca2eb5004c0fb6d6585429612a7871548b9/nh3-0.3.4-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f987cb56458323405e8e5ea827e1befcf141ffa0c0ac797d6d02e6b646056d9a", size = 1103526, upload-time = "2026-03-25T10:57:23.487Z" }, + { url = "https://files.pythonhosted.org/packages/7d/0d/df545070614c1007f0109bb004230226c9000e7857c9785583ec25cda9d7/nh3-0.3.4-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:883d5a6d6ee8078c4afc8e96e022fe579c4c265775ff6ee21e39b8c542cabab3", size = 1068050, upload-time = "2026-03-25T10:57:24.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/d5/17b016df52df052f714c53be71df26a1943551d9931e9383b92c998b88f8/nh3-0.3.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:75643c22f5092d8e209f766ee8108c400bc1e44760fc94d2d638eb138d18f853", size = 1046037, upload-time = "2026-03-25T10:57:25.799Z" }, + { url = "https://files.pythonhosted.org/packages/51/39/49f737907e6ab2b4ca71855d3bd63dd7958862e9c8b94fb4e5b18ccf6988/nh3-0.3.4-cp38-abi3-win32.whl", hash = "sha256:72e4e9ca1c4bd41b4a28b0190edc2e21e3f71496acd36a0162858e1a28db3d7e", size = 609542, upload-time = "2026-03-25T10:57:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/73/4f/af8e9071d7464575a7316831938237ffc9d92d27f163dbdd964b1309cd9b/nh3-0.3.4-cp38-abi3-win_amd64.whl", hash = "sha256:c10b1f0c741e257a5cb2978d6bac86e7c784ab20572724b20c6402c2e24bce75", size = 624244, upload-time = "2026-03-25T10:57:28.302Z" }, + { url = "https://files.pythonhosted.org/packages/44/0c/37695d6b0168f6714b5c492331636a9e6123d6ec22d25876c68d06eab1b8/nh3-0.3.4-cp38-abi3-win_arm64.whl", hash = "sha256:43ad4eedee7e049b9069bc015b7b095d320ed6d167ecec111f877de1540656e9", size = 616649, upload-time = "2026-03-25T10:57:29.623Z" }, +] + [[package]] name = "nodeenv" version = "1.10.0" @@ -1593,15 +1688,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] -[[package]] -name = "paginate" -version = "0.5.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, -] - [[package]] name = "pandas" version = "2.3.0" @@ -1663,7 +1749,7 @@ wheels = [ [[package]] name = "patchsim" -version = "0.1.0" +version = "0.1.0b1" source = { editable = "." } dependencies = [ { name = "matplotlib" }, @@ -1679,15 +1765,24 @@ dependencies = [ [package.optional-dependencies] dev = [ - { name = "mkdocs" }, - { name = "mkdocs-material" }, - { name = "mkdocstrings", extra = ["python"] }, { name = "mypy" }, { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "rich" }, { name = "ruff" }, + { name = "twine" }, +] +docs = [ + { name = "myst-parser" }, + { name = "pydata-sphinx-theme" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +lint = [ + { name = "mypy" }, + { name = "pre-commit" }, + { name = "ruff" }, ] notebook = [ { name = "ipywidgets" }, @@ -1695,33 +1790,43 @@ notebook = [ { name = "plotly" }, { name = "seaborn" }, ] +test = [ + { name = "pytest" }, + { name = "pytest-cov" }, +] [package.metadata] requires-dist = [ { name = "ipywidgets", marker = "extra == 'notebook'" }, { name = "jupyter", marker = "extra == 'notebook'" }, - { name = "matplotlib" }, - { name = "mkdocs", marker = "extra == 'dev'", specifier = ">=1.6.1" }, - { name = "mkdocs-material", marker = "extra == 'dev'", specifier = ">=9.6.12" }, - { name = "mkdocstrings", extras = ["python"], marker = "extra == 'dev'", specifier = ">=0.29.1" }, + { name = "matplotlib", specifier = ">=3.8" }, { name = "mypy", marker = "extra == 'dev'" }, - { name = "networkx" }, - { name = "numpy" }, - { name = "pandas" }, + { name = "mypy", marker = "extra == 'lint'" }, + { name = "myst-parser", marker = "extra == 'docs'", specifier = ">=0.18" }, + { name = "networkx", specifier = ">=3.3" }, + { name = "numpy", specifier = ">=1.26" }, + { name = "pandas", specifier = ">=2.2" }, { name = "plotly", marker = "extra == 'notebook'" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.0.0" }, + { name = "pre-commit", marker = "extra == 'lint'", specifier = ">=3.0.0" }, + { name = "pydata-sphinx-theme", marker = "extra == 'docs'", specifier = ">=0.14" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.5" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.5" }, { name = "pytest-cov", marker = "extra == 'dev'" }, - { name = "pyyaml" }, + { name = "pytest-cov", marker = "extra == 'test'" }, + { name = "pyyaml", specifier = ">=6.0" }, { name = "rich", specifier = ">=14.3.3" }, { name = "rich", marker = "extra == 'dev'", specifier = ">=13.0.0" }, { name = "ruff", marker = "extra == 'dev'" }, - { name = "salib" }, - { name = "scipy" }, + { name = "ruff", marker = "extra == 'lint'" }, + { name = "salib", specifier = ">=1.5" }, + { name = "scipy", specifier = ">=1.11" }, { name = "seaborn", marker = "extra == 'notebook'" }, - { name = "tqdm" }, + { name = "sphinx", marker = "extra == 'docs'", specifier = ">=6.0" }, + { name = "tqdm", specifier = ">=4.66" }, + { name = "twine", marker = "extra == 'dev'", specifier = ">=5.0.0" }, ] -provides-extras = ["dev", "notebook"] +provides-extras = ["test", "docs", "lint", "dev", "notebook"] [[package]] name = "pathspec" @@ -1914,25 +2019,31 @@ wheels = [ ] [[package]] -name = "pygments" -version = "2.19.1" +name = "pydata-sphinx-theme" +version = "0.17.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } +dependencies = [ + { name = "accessible-pygments" }, + { name = "babel" }, + { name = "beautifulsoup4" }, + { name = "docutils" }, + { name = "pygments" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/f7/c74c7100a7f4c0f77b5dcacb7dfdb8fee774fb70e487dd97acba2b930774/pydata_sphinx_theme-0.17.1.tar.gz", hash = "sha256:2cfc1d926c753c77039b7ee53f0ccebcbee5e81f0db61432b01cbb10ad7fd0af", size = 4991415, upload-time = "2026-04-21T13:00:34.263Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, + { url = "https://files.pythonhosted.org/packages/e2/bc/2cb8c78300ce1ace4eeac3b3522218cea2c2053bfa6b4e32cc972a477f9a/pydata_sphinx_theme-0.17.1-py3-none-any.whl", hash = "sha256:320b022d7808bdf5920d9a28e573f27aace9b23e1af6ca103eecc752411df492", size = 6823346, upload-time = "2026-04-21T13:00:31.978Z" }, ] [[package]] -name = "pymdown-extensions" -version = "10.15" +name = "pygments" +version = "2.19.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/08/92/a7296491dbf5585b3a987f3f3fc87af0e632121ff3e490c14b5f2d2b4eb5/pymdown_extensions-10.15.tar.gz", hash = "sha256:0e5994e32155f4b03504f939e501b981d306daf7ec2aa1cd2eb6bd300784f8f7", size = 852320, upload-time = "2025-04-27T23:48:29.183Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/d1/c54e608505776ce4e7966d03358ae635cfd51dff1da6ee421c090dbc797b/pymdown_extensions-10.15-py3-none-any.whl", hash = "sha256:46e99bb272612b0de3b7e7caf6da8dd5f4ca5212c0b273feb9304e236c484e5f", size = 265845, upload-time = "2025-04-27T23:48:27.359Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, ] [[package]] @@ -2032,6 +2143,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b4/f4/f785020090fb050e7fb6d34b780f2231f302609dc964672f72bfaeb59a28/pywin32-310-cp313-cp313-win_arm64.whl", hash = "sha256:e308f831de771482b7cf692a1f308f8fca701b2d8f9dde6cc440c7da17e47b33", size = 8458152, upload-time = "2025-03-17T00:56:07.819Z" }, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + [[package]] name = "pywinpty" version = "2.0.15" @@ -2079,18 +2199,6 @@ wheels = [ { 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]] -name = "pyyaml-env-tag" -version = "1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, -] - [[package]] name = "pyzmq" version = "26.4.0" @@ -2148,6 +2256,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/9c/d8073bd898eb896e94c679abe82e47506e2b750eb261cf6010ced869797c/pyzmq-26.4.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a222ad02fbe80166b0526c038776e8042cd4e5f0dec1489a006a1df47e9040e0", size = 555371, upload-time = "2025-04-04T12:05:20.702Z" }, ] +[[package]] +name = "readme-renderer" +version = "44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "nh3" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056, upload-time = "2024-07-08T15:00:57.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310, upload-time = "2024-07-08T15:00:56.577Z" }, +] + [[package]] name = "referencing" version = "0.36.2" @@ -2177,6 +2299,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, ] +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + [[package]] name = "rfc3339-validator" version = "0.1.4" @@ -2189,6 +2323,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, ] +[[package]] +name = "rfc3986" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, +] + [[package]] name = "rfc3986-validator" version = "0.1.1" @@ -2211,6 +2354,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, ] +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + [[package]] name = "rpds-py" version = "0.25.1" @@ -2387,6 +2539,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" }, ] +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + [[package]] name = "send2trash" version = "1.8.3" @@ -2423,6 +2588,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + [[package]] name = "soupsieve" version = "2.7" @@ -2432,6 +2606,122 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" }, ] +[[package]] +name = "sphinx" +version = "9.0.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version < '3.12'" }, + { name = "babel", marker = "python_full_version < '3.12'" }, + { name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version < '3.12'" }, + { name = "imagesize", marker = "python_full_version < '3.12'" }, + { name = "jinja2", marker = "python_full_version < '3.12'" }, + { name = "packaging", marker = "python_full_version < '3.12'" }, + { name = "pygments", marker = "python_full_version < '3.12'" }, + { name = "requests", marker = "python_full_version < '3.12'" }, + { name = "roman-numerals", marker = "python_full_version < '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version >= '3.12'" }, + { name = "babel", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version >= '3.12'" }, + { name = "imagesize", marker = "python_full_version >= '3.12'" }, + { name = "jinja2", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "requests", marker = "python_full_version >= '3.12'" }, + { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + [[package]] name = "stack-data" version = "0.6.3" @@ -2551,6 +2841,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] +[[package]] +name = "twine" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "id" }, + { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, + { name = "packaging" }, + { name = "readme-renderer" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/a8/949edebe3a82774c1ec34f637f5dd82d1cf22c25e963b7d63771083bbee5/twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf", size = 172262, upload-time = "2025-09-04T15:43:17.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl", hash = "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", size = 42727, upload-time = "2025-09-04T15:43:15.994Z" }, +] + [[package]] name = "types-python-dateutil" version = "2.9.0.20250516" @@ -2611,33 +2921,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/55/896b06bf93a49bec0f4ae2a6f1ed12bd05c8860744ac3a70eda041064e4d/virtualenv-21.1.0-py3-none-any.whl", hash = "sha256:164f5e14c5587d170cf98e60378eb91ea35bf037be313811905d3a24ea33cc07", size = 5825072, upload-time = "2026-02-27T08:49:27.516Z" }, ] -[[package]] -name = "watchdog" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, - { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, - { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, - { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, - { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, - { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, - { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, - { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, - { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, - { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, - { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, - { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, -] - [[package]] name = "wcwidth" version = "0.2.13" @@ -2682,3 +2965,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/41/53/2e0253c5efd69c965 wheels = [ { url = "https://files.pythonhosted.org/packages/ca/51/5447876806d1088a0f8f71e16542bf350918128d0a69437df26047c8e46f/widgetsnbextension-4.0.14-py3-none-any.whl", hash = "sha256:4875a9eaf72fbf5079dc372a51a9f268fc38d46f767cbf85c43a36da5cb9b575", size = 2196503, upload-time = "2025-04-10T13:01:23.086Z" }, ] + +[[package]] +name = "zipp" +version = "3.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, +]