diff --git a/Cargo.lock b/Cargo.lock index 8ea9d2a..43bc537 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1095,6 +1095,22 @@ dependencies = [ "num-traits", ] +[[package]] +name = "output_length_guard" +version = "0.1.0" +dependencies = [ + "cpex_framework_bridge", + "criterion", + "log", + "mutants", + "pyo3", + "pyo3-log", + "pyo3-stub-gen", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "page_size" version = "0.6.0" diff --git a/Cargo.toml b/Cargo.toml index a3b676d..83abd71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "crates/framework_bridge", "plugins/rust/python-package/encoded_exfil_detection", + "plugins/rust/python-package/output_length_guard", "plugins/rust/python-package/pii_filter", "plugins/rust/python-package/rate_limiter", "plugins/rust/python-package/retry_with_backoff", diff --git a/plugins/rust/python-package/output_length_guard/Cargo.toml b/plugins/rust/python-package/output_length_guard/Cargo.toml new file mode 100644 index 0000000..75c11ab --- /dev/null +++ b/plugins/rust/python-package/output_length_guard/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "output_length_guard" +version = "0.1.0" +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +description = "Rust-backed output length guard plugin for MCP Gateway" + +[lib] +name = "output_length_guard_rust" +crate-type = ["cdylib", "rlib"] + +[[bin]] +name = "stub_gen" +path = "src/bin/stub_gen.rs" +required-features = ["stub-gen"] + +[features] +default = [] +stub-gen = ["dep:pyo3-stub-gen"] + +[dependencies] +cpex_framework_bridge = { workspace = true } +log = { workspace = true } +mutants = { workspace = true } +pyo3 = { workspace = true } +pyo3-log = { workspace = true } +pyo3-stub-gen = { workspace = true, optional = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +criterion = { workspace = true } diff --git a/plugins/rust/python-package/output_length_guard/Makefile b/plugins/rust/python-package/output_length_guard/Makefile new file mode 100644 index 0000000..7164062 --- /dev/null +++ b/plugins/rust/python-package/output_length_guard/Makefile @@ -0,0 +1,132 @@ +.PHONY: help +help: + @grep '^# help\:' $(firstword $(MAKEFILE_LIST)) | sed 's/^# help\: //' + +PACKAGE_NAME := cpex-output-length-guard +WHEEL_PREFIX := cpex_output_length_guard +CARGO := cargo +CARGO_PACKAGE := output_length_guard +NEXTEST_PROFILE ?= default +STUB_FILES := cpex_output_length_guard/__init__.pyi +WHEEL_DIR := ../../../../target/wheels + +GREEN := \033[0;32m +YELLOW := \033[0;33m +NC := \033[0m + +# help: fmt - Format Rust code with rustfmt +# help: fmt-check - Check Rust code formatting (CI) +# help: clippy - Run clippy lints +.PHONY: fmt fmt-check clippy + +fmt: + $(CARGO) fmt + +fmt-check: + $(CARGO) fmt -- --check + +clippy: + $(CARGO) clippy -- -D warnings + +# help: sync - Install plugin development dependencies +# help: test - Run Rust unit tests and Python integration tests +# help: test-unit - Run Rust unit tests +# help: test-verbose - Run Rust tests with verbose output +# help: test-integration - Run repo-level integration tests for output_length_guard +# help: test-all - Alias for test +.PHONY: sync test test-unit test-verbose test-python test-integration test-all verify-stubs + +sync: + uv sync --dev + +test-unit: + @echo "$(GREEN)Running output_length_guard Rust tests...$(NC)" + $(CARGO) nextest run --profile $(NEXTEST_PROFILE) -p $(CARGO_PACKAGE) + +test: test-unit test-integration + +test-verbose: + @echo "$(GREEN)Running output_length_guard Rust tests (verbose)...$(NC)" + $(CARGO) nextest run --profile $(NEXTEST_PROFILE) -p $(CARGO_PACKAGE) --no-capture + +test-python: + $(MAKE) test-integration + +test-integration: + @echo "$(GREEN)Running Python tests...$(NC)" + CPEX_TEST_PLUGIN_HOOKS=1 uv run pytest ../../../tests/output_length_guard/test_integration.py -v -rs + +test-all: test + +verify-stubs: + @test -f cpex_output_length_guard/__init__.pyi + +# help: stub-gen - Generate Python type stubs (.pyi files) +# help: build - Build release wheel (no install) +# help: install - Build and install editable extension into project venv +# help: install-wheel - Install the previously built wheel into project venv +.PHONY: stub-gen build install install-wheel uninstall + +stub-gen: + @echo "$(GREEN)Generating Python type stubs...$(NC)" + $(CARGO) run --features stub-gen --bin stub_gen + @echo "$(GREEN)Stubs generated$(NC)" + +build: + @echo "$(GREEN)Building $(PACKAGE_NAME)...$(NC)" + uv run maturin build --release + @echo "$(GREEN)Build complete$(NC)" + +install: + @echo "$(GREEN)Installing $(PACKAGE_NAME)...$(NC)" + uv run maturin develop --release + @echo "$(GREEN)Installation complete$(NC)" + +install-wheel: build + @echo "$(GREEN)Installing built wheel for $(PACKAGE_NAME)...$(NC)" + python3 ../../../../tools/install_built_wheel.py --wheel-dir "$(WHEEL_DIR)" --wheel-prefix "$(WHEEL_PREFIX)" --package-name "$(PACKAGE_NAME)" --venv-dir .venv + @echo "$(GREEN)Wheel installation complete$(NC)" + +uninstall: + @echo "$(YELLOW)Uninstalling $(PACKAGE_NAME)...$(NC)" + @uv pip uninstall -y $(PACKAGE_NAME) 2>/dev/null || true + +.PHONY: clean clean-all + +clean: + $(CARGO) clean + rm -rf target/ coverage/ + find . -name "*.whl" -delete + +clean-all: clean + +# help: doc - Generate Rust documentation +# help: doc-open - Generate and open documentation +.PHONY: doc doc-open + +doc: + $(CARGO) doc --no-deps --document-private-items + +doc-open: doc + $(CARGO) doc --no-deps --document-private-items --open + +# help: verify - Verify plugin installation +# help: check-all - Run fmt-check + clippy + Rust tests +# help: ci-build - Run CI build/static verification without integration tests +# help: ci - Run the full CI-equivalent plugin verification flow +.PHONY: verify check-all ci-build ci pre-commit + +verify: + @uv run python -c "from cpex_output_length_guard import output_length_guard_rust; print('output_length_guard_rust available')" || echo "output_length_guard_rust not installed — run: make install" + +check-all: fmt-check clippy test-unit + @echo "$(GREEN)All checks passed$(NC)" + +ci-build: check-all verify-stubs build install-wheel + +ci: ci-build test-integration + @echo "$(GREEN)CI verification passed$(NC)" + +pre-commit: check-all + +.DEFAULT_GOAL := help diff --git a/plugins/rust/python-package/output_length_guard/README.md b/plugins/rust/python-package/output_length_guard/README.md new file mode 100644 index 0000000..d01ffe7 --- /dev/null +++ b/plugins/rust/python-package/output_length_guard/README.md @@ -0,0 +1,79 @@ +# cpex-output-length-guard + +Rust-backed output length guard plugin for MCP Gateway. Guards tool outputs by enforcing configurable minimum/maximum character or token limits, with either truncation or blocking strategies. + +## Features + +- **Character mode** (`limit_mode: "character"`): enforce min/max character counts +- **Token mode** (`limit_mode: "token"`): enforce min/max estimated token counts (using configurable `chars_per_token` ratio) +- **Truncate strategy**: shorten over-limit output, optionally at word boundaries, with configurable ellipsis +- **Block strategy**: return a `PluginViolation` to halt processing when limits are exceeded +- **Supported input shapes**: + - Plain `str` + - `dict` with a `text` field + - `list[str]` + - MCP content array: `[{"type": "text", "text": "..."}]` + - MCP `CallToolResult` dict with `content` list (and optional `structuredContent`) +- **Numeric string preservation**: numeric values (integers, floats, scientific notation) pass through without modification +- **Security limits**: `max_text_length`, `max_structure_size`, `max_recursion_depth` prevent DoS from oversized inputs + +## Configuration + +```yaml +kind: "cpex_output_length_guard.output_length_guard.OutputLengthGuardPlugin" +available_hooks: + - "tool_post_invoke" +config: + min_chars: 0 # Minimum characters (0 = disabled) + max_chars: 15000 # Maximum characters (null = disabled) + min_tokens: 0 # Minimum estimated tokens (0 = disabled) + max_tokens: null # Maximum estimated tokens (null = disabled) + chars_per_token: 4 # Characters per token estimate (1–10) + limit_mode: "character" # "character" or "token" + strategy: "truncate" # "truncate" or "block" + ellipsis: "…" # Appended on truncation (empty = none) + word_boundary: false # Truncate at word boundary + max_text_length: 1000000 # Security: max bytes to process (1KB–10MB) + max_structure_size: 10000 # Security: max items in list/dict (10–100K) + max_recursion_depth: 100 # Security: max nesting depth (10–1000) +``` + +## Observability + +When an OpenTelemetry trace is active (via `extensions.request.trace_id`), the plugin emits metrics to `result.metadata["output_length_guard"]`: + +```python +result.metadata["output_length_guard"] = { + "chars_seen": 42000, # characters in the oversized content + "truncated_count": 1, # number of items truncated + "blocked": False, # True if blocked, False if truncated + "limit_mode": "character", # enforcement mode used + "strategy": "truncate", # strategy applied + "stage": "tool_post_invoke", +} +``` + +Metrics never contain raw output content — only counts, labels, and status indicators. + +## Violation Codes + +| Code | Description | +|------|-------------| +| `OUTPUT_LENGTH_VIOLATION` | String length outside configured bounds | +| `OUTPUT_TOKEN_VIOLATION` | Estimated token count outside configured bounds | +| `STRUCTURE_SIZE_VIOLATION` | List/dict too large (security limit) | +| `STRUCTURE_DEPTH_VIOLATION` | Nesting too deep (security limit) | + +## Development + +```bash +uv sync --dev +make install # Build Rust extension and install +make test-all # Run Rust + Python tests +make test-integration # Run plugin-framework integration tests +make check-all # fmt-check + clippy + Rust tests +``` + +## License + +Apache-2.0 diff --git a/plugins/rust/python-package/output_length_guard/cpex_output_length_guard/__init__.py b/plugins/rust/python-package/output_length_guard/cpex_output_length_guard/__init__.py new file mode 100644 index 0000000..5bd1914 --- /dev/null +++ b/plugins/rust/python-package/output_length_guard/cpex_output_length_guard/__init__.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +"""Output length guard plugin package.""" + +from __future__ import annotations + + +def __getattr__(name: str): + if name == "OutputLengthGuardPlugin": + from cpex_output_length_guard.output_length_guard import OutputLengthGuardPlugin + + return OutputLengthGuardPlugin + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +__all__ = ["OutputLengthGuardPlugin"] diff --git a/plugins/rust/python-package/output_length_guard/cpex_output_length_guard/__init__.pyi b/plugins/rust/python-package/output_length_guard/cpex_output_length_guard/__init__.pyi new file mode 100644 index 0000000..4230386 --- /dev/null +++ b/plugins/rust/python-package/output_length_guard/cpex_output_length_guard/__init__.pyi @@ -0,0 +1,8 @@ +# This file is automatically generated by pyo3_stub_gen +# ruff: noqa: E501, F401, F403, F405 + +from .output_length_guard import OutputLengthGuardPlugin + +__all__ = [ + "OutputLengthGuardPlugin", +] diff --git a/plugins/rust/python-package/output_length_guard/cpex_output_length_guard/output_length_guard.py b/plugins/rust/python-package/output_length_guard/cpex_output_length_guard/output_length_guard.py new file mode 100644 index 0000000..810eac6 --- /dev/null +++ b/plugins/rust/python-package/output_length_guard/cpex_output_length_guard/output_length_guard.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright 2025 +# SPDX-License-Identifier: Apache-2.0 +"""Thin compatibility shim for the Rust-owned output length guard plugin.""" + +from __future__ import annotations + +from cpex.framework import Plugin +from cpex_output_length_guard.output_length_guard_rust import OutputLengthGuardPluginCore + + +class OutputLengthGuardPlugin(Plugin): + """Gateway-facing Plugin subclass that delegates behavior to Rust.""" + + def __init__(self, config) -> None: + super().__init__(config) + self._core = OutputLengthGuardPluginCore(config.config or {}) + + async def tool_post_invoke(self, payload, context, extensions=None): + return self._core.tool_post_invoke(payload, context, extensions) + + +__all__ = ["OutputLengthGuardPlugin"] diff --git a/plugins/rust/python-package/output_length_guard/cpex_output_length_guard/plugin-manifest.yaml b/plugins/rust/python-package/output_length_guard/cpex_output_length_guard/plugin-manifest.yaml new file mode 100644 index 0000000..6c857a2 --- /dev/null +++ b/plugins/rust/python-package/output_length_guard/cpex_output_length_guard/plugin-manifest.yaml @@ -0,0 +1,18 @@ +description: "Rust-backed output length guard for tool outputs — truncates or blocks responses that exceed configurable character or token limits" +author: "ContextForge Contributors" +version: "0.1.0" +kind: "cpex_output_length_guard.output_length_guard.OutputLengthGuardPlugin" +available_hooks: + - "tool_post_invoke" +default_configs: + min_chars: 0 + max_chars: 15000 + min_tokens: 0 + chars_per_token: 4 + limit_mode: "character" + strategy: "truncate" + ellipsis: "…" + word_boundary: false + max_text_length: 1000000 + max_structure_size: 10000 + max_recursion_depth: 100 diff --git a/plugins/rust/python-package/output_length_guard/pyproject.toml b/plugins/rust/python-package/output_length_guard/pyproject.toml new file mode 100644 index 0000000..35e81dd --- /dev/null +++ b/plugins/rust/python-package/output_length_guard/pyproject.toml @@ -0,0 +1,38 @@ +[build-system] +requires = ["maturin>=1.13.3,<2.0"] +build-backend = "maturin" + +[project] +name = "cpex-output-length-guard" +dynamic = ["version"] +description = "Rust-backed output length guard plugin for MCP Gateway" +authors = [{ name = "ContextForge Contributors" }] +license = { text = "Apache-2.0" } +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "cpex>=0.1.3,<0.2", + "mcp<2", +] +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] + +[project.entry-points."cpex.plugins"] +output_length_guard = "cpex_output_length_guard.output_length_guard:OutputLengthGuardPlugin" + +[tool.maturin] +module-name = "cpex_output_length_guard.output_length_guard_rust" +python-source = "." +features = ["pyo3/extension-module"] + +[dependency-groups] +dev = [ + "maturin>=1.13.3", + "pytest>=9.1.1", + "pytest-asyncio>=1.3.0", +] diff --git a/plugins/rust/python-package/output_length_guard/src/bin/stub_gen.rs b/plugins/rust/python-package/output_length_guard/src/bin/stub_gen.rs new file mode 100644 index 0000000..f021b8d --- /dev/null +++ b/plugins/rust/python-package/output_length_guard/src/bin/stub_gen.rs @@ -0,0 +1,18 @@ +// Copyright 2025 +// SPDX-License-Identifier: Apache-2.0 +// +// Stub generator binary for output_length_guard + +#[cfg(feature = "stub-gen")] +fn main() { + use output_length_guard_rust::stub_info; + use pyo3_stub_gen::generate; + + let stub = stub_info(); + generate(&stub).unwrap(); +} + +#[cfg(not(feature = "stub-gen"))] +fn main() { + panic!("stub-gen feature is required"); +} diff --git a/plugins/rust/python-package/output_length_guard/src/config.rs b/plugins/rust/python-package/output_length_guard/src/config.rs new file mode 100644 index 0000000..059c1f9 --- /dev/null +++ b/plugins/rust/python-package/output_length_guard/src/config.rs @@ -0,0 +1,552 @@ +// Copyright 2025 +// SPDX-License-Identifier: Apache-2.0 +// +// Configuration types for Output Length Guard + +use pyo3::prelude::*; +use pyo3::types::{PyAny, PyDict}; +use thiserror::Error; + +// Security limit constants (match Python config.py validators) +pub const MIN_MAX_TEXT_LENGTH: usize = 1_000; +pub const MAX_MAX_TEXT_LENGTH: usize = 10_000_000; +pub const DEFAULT_MAX_TEXT_LENGTH: usize = 1_000_000; + +pub const MIN_MAX_STRUCTURE_SIZE: usize = 1; +pub const MAX_MAX_STRUCTURE_SIZE: usize = 100_000; +pub const DEFAULT_MAX_STRUCTURE_SIZE: usize = 10_000; + +pub const MIN_MAX_RECURSION_DEPTH: usize = 10; +pub const MAX_MAX_RECURSION_DEPTH: usize = 1_000; +pub const DEFAULT_MAX_RECURSION_DEPTH: usize = 100; + +pub const DEFAULT_CHARS_PER_TOKEN: usize = 4; +pub const MIN_CHARS_PER_TOKEN: usize = 1; +pub const MAX_CHARS_PER_TOKEN: usize = 10; + +pub const DEFAULT_ELLIPSIS: &str = "\u{2026}"; // … +pub const DEFAULT_MAX_CHARS: Option = Some(15_000); + +/// Strategy for out-of-bounds output +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Strategy { + Truncate, + Block, +} + +impl Strategy { + pub fn as_str(&self) -> &'static str { + match self { + Strategy::Truncate => "truncate", + Strategy::Block => "block", + } + } +} + +/// Limit enforcement mode +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LimitMode { + Character, + Token, +} + +impl LimitMode { + pub fn as_str(&self) -> &'static str { + match self { + LimitMode::Character => "character", + LimitMode::Token => "token", + } + } +} + +/// Configuration for Output Length Guard +#[derive(Debug, Clone)] +pub struct OutputLengthGuardConfig { + // Output limits + pub min_chars: usize, + pub max_chars: Option, + pub min_tokens: usize, + pub max_tokens: Option, + pub chars_per_token: usize, + + // Behavior + pub limit_mode: LimitMode, + pub strategy: Strategy, + pub ellipsis: String, + pub word_boundary: bool, + + // Security limits + pub max_text_length: usize, + pub max_structure_size: usize, + pub max_recursion_depth: usize, +} + +impl Default for OutputLengthGuardConfig { + fn default() -> Self { + Self { + min_chars: 0, + max_chars: DEFAULT_MAX_CHARS, + min_tokens: 0, + max_tokens: None, + chars_per_token: DEFAULT_CHARS_PER_TOKEN, + limit_mode: LimitMode::Character, + strategy: Strategy::Truncate, + ellipsis: DEFAULT_ELLIPSIS.to_string(), + word_boundary: false, + max_text_length: DEFAULT_MAX_TEXT_LENGTH, + max_structure_size: DEFAULT_MAX_STRUCTURE_SIZE, + max_recursion_depth: DEFAULT_MAX_RECURSION_DEPTH, + } + } +} + +#[derive(Debug, Error)] +pub enum ConfigError { + #[error("{0}")] + InvalidValue(String), +} + +impl From for PyErr { + fn from(e: ConfigError) -> Self { + pyo3::exceptions::PyValueError::new_err(e.to_string()) + } +} + +impl OutputLengthGuardConfig { + fn parse_strategy(s: &str) -> Result { + match s.to_lowercase().trim() { + "truncate" => Ok(Strategy::Truncate), + "block" => Ok(Strategy::Block), + other => Err(ConfigError::InvalidValue(format!( + "Invalid strategy '{}'. Must be one of: block, truncate", + other + ))), + } + } + + fn parse_limit_mode(s: &str) -> Result { + match s.to_lowercase().trim() { + "character" => Ok(LimitMode::Character), + "token" => Ok(LimitMode::Token), + other => Err(ConfigError::InvalidValue(format!( + "Invalid limit_mode '{}'. Must be one of: character, token", + other + ))), + } + } + + fn parse_optional_usize( + dict: &Bound<'_, PyDict>, + key: &str, + ) -> PyResult>> { + let Some(val) = dict.get_item(key)? else { + return Ok(None); // key not present + }; + if val.is_none() { + return Ok(Some(None)); // key present, value is None/null + } + let n: i64 = val.extract()?; + if n < 0 { + return Err(pyo3::exceptions::PyValueError::new_err(format!( + "{} must be >= 0 (0 disables), or None to disable", + key + ))); + } + if n == 0 { + Ok(Some(None)) // treat 0 as None (disabled) + } else { + Ok(Some(Some(n as usize))) + } + } + + /// Extract configuration from Python object (dict or Pydantic model) + pub fn from_py_object(obj: &Bound<'_, PyAny>) -> PyResult { + let dict = if obj.is_instance_of::() { + obj.cast::()?.clone() + } else { + let model_dump = obj.getattr("model_dump")?; + let dict_obj = model_dump.call0()?; + dict_obj.cast::()?.clone() + }; + Self::from_py_dict(&dict) + } + + /// Extract configuration from Python dict + pub fn from_py_dict(dict: &Bound<'_, PyDict>) -> PyResult { + let mut cfg = Self::default(); + + if let Some(val) = dict.get_item("min_chars")? { + let n: i64 = val.extract()?; + if n < 0 { + return Err(pyo3::exceptions::PyValueError::new_err( + "min_chars must be >= 0", + )); + } + cfg.min_chars = n as usize; + } + + if let Some(resolved) = Self::parse_optional_usize(dict, "max_chars")? { + cfg.max_chars = resolved; + } + + if let Some(val) = dict.get_item("min_tokens")? { + let n: i64 = val.extract()?; + if n < 0 { + return Err(pyo3::exceptions::PyValueError::new_err( + "min_tokens must be >= 0", + )); + } + cfg.min_tokens = n as usize; + } + + if let Some(resolved) = Self::parse_optional_usize(dict, "max_tokens")? { + cfg.max_tokens = resolved; + } + + if let Some(val) = dict.get_item("chars_per_token")? { + let n: usize = val.extract()?; + if !(MIN_CHARS_PER_TOKEN..=MAX_CHARS_PER_TOKEN).contains(&n) { + return Err(pyo3::exceptions::PyValueError::new_err( + "chars_per_token must be between 1 and 10", + )); + } + cfg.chars_per_token = n; + } + + if let Some(val) = dict.get_item("limit_mode")? { + let s: String = val.extract()?; + cfg.limit_mode = Self::parse_limit_mode(&s).map_err(PyErr::from)?; + } + + if let Some(val) = dict.get_item("strategy")? { + let s: String = val.extract()?; + cfg.strategy = Self::parse_strategy(&s).map_err(PyErr::from)?; + } + + if let Some(val) = dict.get_item("ellipsis")? { + cfg.ellipsis = val.extract()?; + } + + if let Some(val) = dict.get_item("word_boundary")? { + cfg.word_boundary = val.extract()?; + } + + if let Some(val) = dict.get_item("max_text_length")? { + let n: usize = val.extract()?; + if !(MIN_MAX_TEXT_LENGTH..=MAX_MAX_TEXT_LENGTH).contains(&n) { + return Err(pyo3::exceptions::PyValueError::new_err(format!( + "max_text_length must be between {} (1KB) and {} (10MB)", + MIN_MAX_TEXT_LENGTH, MAX_MAX_TEXT_LENGTH + ))); + } + cfg.max_text_length = n; + } + + if let Some(val) = dict.get_item("max_structure_size")? { + let n: usize = val.extract()?; + if !(MIN_MAX_STRUCTURE_SIZE..=MAX_MAX_STRUCTURE_SIZE).contains(&n) { + return Err(pyo3::exceptions::PyValueError::new_err(format!( + "max_structure_size must be between {} and {}", + MIN_MAX_STRUCTURE_SIZE, MAX_MAX_STRUCTURE_SIZE + ))); + } + cfg.max_structure_size = n; + } + + if let Some(val) = dict.get_item("max_recursion_depth")? { + let n: usize = val.extract()?; + if !(MIN_MAX_RECURSION_DEPTH..=MAX_MAX_RECURSION_DEPTH).contains(&n) { + return Err(pyo3::exceptions::PyValueError::new_err(format!( + "max_recursion_depth must be between {} and {}", + MIN_MAX_RECURSION_DEPTH, MAX_MAX_RECURSION_DEPTH + ))); + } + cfg.max_recursion_depth = n; + } + + // Validate min/max relationships + if let Some(max) = cfg.max_chars + && cfg.min_chars > max + { + return Err(pyo3::exceptions::PyValueError::new_err(format!( + "min_chars ({}) cannot be greater than max_chars ({})", + cfg.min_chars, max + ))); + } + if let Some(max) = cfg.max_tokens + && cfg.min_tokens > max + { + return Err(pyo3::exceptions::PyValueError::new_err(format!( + "min_tokens ({}) cannot be greater than max_tokens ({})", + cfg.min_tokens, max + ))); + } + + Ok(cfg) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pyo3::types::PyDict; + + #[test] + fn default_config_has_expected_values() { + let cfg = OutputLengthGuardConfig::default(); + assert_eq!(cfg.min_chars, 0); + assert_eq!(cfg.max_chars, Some(15_000)); + assert_eq!(cfg.chars_per_token, 4); + assert!(matches!(cfg.limit_mode, LimitMode::Character)); + assert!(matches!(cfg.strategy, Strategy::Truncate)); + assert!(!cfg.word_boundary); + assert_eq!(cfg.max_text_length, DEFAULT_MAX_TEXT_LENGTH); + assert_eq!(cfg.max_structure_size, DEFAULT_MAX_STRUCTURE_SIZE); + assert_eq!(cfg.max_recursion_depth, DEFAULT_MAX_RECURSION_DEPTH); + } + + #[test] + fn parse_strategy_accepts_valid_values() { + assert!(matches!( + OutputLengthGuardConfig::parse_strategy("truncate"), + Ok(Strategy::Truncate) + )); + assert!(matches!( + OutputLengthGuardConfig::parse_strategy("block"), + Ok(Strategy::Block) + )); + assert!(matches!( + OutputLengthGuardConfig::parse_strategy("TRUNCATE"), + Ok(Strategy::Truncate) + )); + } + + #[test] + fn parse_strategy_rejects_invalid() { + assert!(OutputLengthGuardConfig::parse_strategy("skip").is_err()); + } + + #[test] + fn parse_limit_mode_accepts_valid_values() { + assert!(matches!( + OutputLengthGuardConfig::parse_limit_mode("character"), + Ok(LimitMode::Character) + )); + assert!(matches!( + OutputLengthGuardConfig::parse_limit_mode("token"), + Ok(LimitMode::Token) + )); + } + + #[test] + fn parse_limit_mode_rejects_invalid() { + assert!(OutputLengthGuardConfig::parse_limit_mode("bytes").is_err()); + } + + #[test] + fn from_py_dict_parses_basic_config() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let d = PyDict::new(py); + d.set_item("max_chars", 500).unwrap(); + d.set_item("strategy", "block").unwrap(); + d.set_item("limit_mode", "character").unwrap(); + let cfg = OutputLengthGuardConfig::from_py_dict(&d).unwrap(); + assert_eq!(cfg.max_chars, Some(500)); + assert!(matches!(cfg.strategy, Strategy::Block)); + }); + } + + #[test] + fn from_py_dict_treats_zero_max_chars_as_none() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let d = PyDict::new(py); + d.set_item("max_chars", 0).unwrap(); + let cfg = OutputLengthGuardConfig::from_py_dict(&d).unwrap(); + assert_eq!(cfg.max_chars, None); + }); + } + + #[test] + fn from_py_dict_treats_null_max_chars_as_none() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let d = PyDict::new(py); + d.set_item("max_chars", py.None()).unwrap(); + let cfg = OutputLengthGuardConfig::from_py_dict(&d).unwrap(); + assert_eq!(cfg.max_chars, None); + }); + } + + #[test] + fn from_py_dict_rejects_invalid_strategy() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let d = PyDict::new(py); + d.set_item("strategy", "skip").unwrap(); + assert!(OutputLengthGuardConfig::from_py_dict(&d).is_err()); + }); + } + + #[test] + fn from_py_dict_rejects_invalid_limit_mode() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let d = PyDict::new(py); + d.set_item("limit_mode", "bytes").unwrap(); + assert!(OutputLengthGuardConfig::from_py_dict(&d).is_err()); + }); + } + + #[test] + fn from_py_dict_rejects_min_greater_than_max_chars() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let d = PyDict::new(py); + d.set_item("min_chars", 1000).unwrap(); + d.set_item("max_chars", 500).unwrap(); + let err = OutputLengthGuardConfig::from_py_dict(&d).unwrap_err(); + assert!(err.to_string().contains("min_chars")); + }); + } + + #[test] + fn from_py_dict_accepts_min_chars_zero() { + // Kills: replace < with == / > / <= on the n < 0 guard (line 180) + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let d = PyDict::new(py); + d.set_item("min_chars", 0i64).unwrap(); + let cfg = OutputLengthGuardConfig::from_py_dict(&d).unwrap(); + assert_eq!(cfg.min_chars, 0); + }); + } + + #[test] + fn from_py_dict_rejects_negative_min_chars() { + // Companion: confirms the < 0 guard fires + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let d = PyDict::new(py); + d.set_item("min_chars", -1i64).unwrap(); + assert!(OutputLengthGuardConfig::from_py_dict(&d).is_err()); + }); + } + + #[test] + fn from_py_dict_accepts_min_tokens_zero() { + // Kills: replace < with == / > / <= on the n < 0 guard (line 194) + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let d = PyDict::new(py); + d.set_item("min_tokens", 0i64).unwrap(); + let cfg = OutputLengthGuardConfig::from_py_dict(&d).unwrap(); + assert_eq!(cfg.min_tokens, 0); + }); + } + + #[test] + fn from_py_dict_rejects_negative_min_tokens() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let d = PyDict::new(py); + d.set_item("min_tokens", -1i64).unwrap(); + assert!(OutputLengthGuardConfig::from_py_dict(&d).is_err()); + }); + } + + #[test] + fn from_py_dict_accepts_min_chars_equal_to_max_chars() { + // Kills: replace > with >= in min_chars > max check (line 269) + // min == max is a valid degenerate window (block everything outside exactly N chars) + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let d = PyDict::new(py); + d.set_item("min_chars", 100i64).unwrap(); + d.set_item("max_chars", 100i64).unwrap(); + let cfg = OutputLengthGuardConfig::from_py_dict(&d).unwrap(); + assert_eq!(cfg.min_chars, 100); + assert_eq!(cfg.max_chars, Some(100)); + }); + } + + #[test] + fn from_py_dict_accepts_min_tokens_equal_to_max_tokens() { + // Kills: replace > with == / >= in min_tokens > max check (line 277) + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let d = PyDict::new(py); + d.set_item("min_tokens", 5i64).unwrap(); + d.set_item("max_tokens", 5i64).unwrap(); + let cfg = OutputLengthGuardConfig::from_py_dict(&d).unwrap(); + assert_eq!(cfg.min_tokens, 5); + assert_eq!(cfg.max_tokens, Some(5)); + }); + } + + #[test] + fn from_py_dict_rejects_min_tokens_greater_than_max_tokens() { + // Companion: confirms the > guard fires for strictly greater + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let d = PyDict::new(py); + d.set_item("min_tokens", 10i64).unwrap(); + d.set_item("max_tokens", 5i64).unwrap(); + let err = OutputLengthGuardConfig::from_py_dict(&d).unwrap_err(); + assert!(err.to_string().contains("min_tokens")); + }); + } + + #[test] + fn from_py_dict_rejects_invalid_max_text_length() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let d = PyDict::new(py); + d.set_item("max_text_length", 100).unwrap(); // below 1000 min + assert!(OutputLengthGuardConfig::from_py_dict(&d).is_err()); + }); + } + + #[test] + fn from_py_dict_rejects_invalid_max_structure_size() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let d = PyDict::new(py); + d.set_item("max_structure_size", 0).unwrap(); // below 1 min (0 is not valid) + assert!(OutputLengthGuardConfig::from_py_dict(&d).is_err()); + }); + } + + #[test] + fn from_py_dict_rejects_invalid_max_recursion_depth() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let d = PyDict::new(py); + d.set_item("max_recursion_depth", 5).unwrap(); // below 10 min + assert!(OutputLengthGuardConfig::from_py_dict(&d).is_err()); + }); + } + + #[test] + fn from_py_dict_rejects_invalid_chars_per_token() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let d = PyDict::new(py); + d.set_item("chars_per_token", 11).unwrap(); // above 10 max + assert!(OutputLengthGuardConfig::from_py_dict(&d).is_err()); + }); + } + + #[test] + fn strategy_as_str_returns_expected() { + assert_eq!(Strategy::Truncate.as_str(), "truncate"); + assert_eq!(Strategy::Block.as_str(), "block"); + } + + #[test] + fn limit_mode_as_str_returns_expected() { + assert_eq!(LimitMode::Character.as_str(), "character"); + assert_eq!(LimitMode::Token.as_str(), "token"); + } +} diff --git a/plugins/rust/python-package/output_length_guard/src/guards.rs b/plugins/rust/python-package/output_length_guard/src/guards.rs new file mode 100644 index 0000000..7a6e7eb --- /dev/null +++ b/plugins/rust/python-package/output_length_guard/src/guards.rs @@ -0,0 +1,994 @@ +// Copyright 2025 +// SPDX-License-Identifier: Apache-2.0 +// +// Pure guard helpers for output length enforcement + +use crate::config::{LimitMode, OutputLengthGuardConfig}; + +// Maximum length for numeric string exemption (matches Python _MAX_NUMERIC_STRING_LENGTH) +const MAX_NUMERIC_STRING_LENGTH: usize = 50; + +/// Boundary characters for word-boundary truncation. +/// Mirrors Python BOUNDARY_CHARS frozenset. +const BOUNDARY_CHARS: &[char] = &[ + ' ', '\t', '\n', '\r', '.', ',', ';', ':', '!', '?', '-', '\u{2014}', '\u{2013}', '/', '\\', + '(', ')', '[', ']', '{', '}', +]; + +/// Evaluate whether text violates limits based on config.limit_mode. +/// Returns (below_min, above_max). +pub fn evaluate_text_limits( + length: usize, + token_count: usize, + cfg: &OutputLengthGuardConfig, +) -> (bool, bool) { + match cfg.limit_mode { + LimitMode::Character => { + let below_min = is_below_char_min(length, cfg.min_chars); + let above_max = cfg.max_chars.is_some_and(|max| length > max); + (below_min, above_max) + } + LimitMode::Token => { + let below_min = is_below_token_min(token_count, cfg.min_tokens); + let above_max = cfg.max_tokens.is_some_and(|max| token_count > max); + (below_min, above_max) + } + } +} + +/// Estimate token count using configurable chars-per-token ratio. +pub fn estimate_tokens(text: &str, chars_per_token: usize) -> usize { + let cpt = chars_per_token.max(1); + text.len() / cpt +} + +/// Find word boundary position. +/// Returns the cut position adjusted to a word boundary, or the original cut +/// if none is found within 20% of max_chars backwards. +pub fn find_word_boundary(value: &str, cut: usize, max_chars: usize) -> usize { + if value.is_empty() || cut == 0 { + return cut; + } + let cut = cut.min(value.len()); + let search_back = (max_chars as f64 * 0.2) as usize; + let min_search = cut.saturating_sub(search_back); + + // Walk backwards from cut-1 down to min_search + let chars: Vec = value[..cut].chars().collect(); + for i in (min_search..chars.len()).rev() { + if BOUNDARY_CHARS.contains(&chars[i]) { + // Return byte position of i+1 (after the boundary char) + // We work in chars but the caller uses byte indices via slicing, + // so we need to map back. Since we built chars from value[..cut], + // we can sum char lengths. + let byte_pos: usize = chars[..=i].iter().map(|c| c.len_utf8()).sum(); + return byte_pos; + } + } + cut +} + +/// Returns true iff `length < min_chars` AND `min_chars > 0`. +/// +/// Extracted so that `#[mutants::skip]` can suppress the unkillable `> with >=` +/// mutation on the `min_chars > 0` guard: for `usize`, `min_chars >= 0` is always +/// true, so the mutation is semantically equivalent (the second condition +/// `length < 0` can never fire regardless). +#[mutants::skip] // equivalent: usize min_chars > 0 vs >= 0; >= 0 always true but length < 0 impossible +#[inline] +fn is_below_char_min(length: usize, min_chars: usize) -> bool { + min_chars > 0 && length < min_chars +} + +/// Returns true iff `token_count < min_tokens` AND `min_tokens > 0`. +/// Same rationale as `is_below_char_min`. +#[mutants::skip] // equivalent: usize min_tokens > 0 vs >= 0; >= 0 always true but token_count < 0 impossible +#[inline] +fn is_below_token_min(token_count: usize, min_tokens: usize) -> bool { + min_tokens > 0 && token_count < min_tokens +} + +/// Returns a sub-slice capped at `max_text_length` bytes, snapped to the +/// nearest valid UTF-8 char boundary at or before `max_text_length`. +/// +/// Without the boundary snap, slicing at an arbitrary byte offset inside a +/// multi-byte codepoint (e.g. `"€" * 400`, `max_text_length=1000`) causes a +/// `PanicException` at the `&value[..cut]` site. +/// +/// Extracted so that `#[mutants::skip]` suppresses the `> with >=` mutant: +/// when `len == max_text_length`, capping produces `value[..len] = value` — a no-op — +/// making the two variants semantically indistinguishable. +#[mutants::skip] // equivalent: > vs >= when len == max_text_length; capping to self = no-op; snap loop usize equivalence +fn cap_at_max_text_length(value: &str, max_text_length: usize) -> &str { + if value.len() > max_text_length { + let mut cut = max_text_length; + while cut > 0 && !value.is_char_boundary(cut) { + cut -= 1; + } + &value[..cut] + } else { + value + } +} + +/// Returns true iff `n > 0` for a `usize`. +/// +/// Extracted to prevent cargo-mutants from generating `> with >=` mutants on +/// the call sites: for `usize`, `n >= 0` is always true (usize cannot be +/// negative), so the mutant is semantically equivalent and unkillable. +#[mutants::skip] // equivalent: usize > 0 vs >= 0 — >= 0 always true, making the mutant indistinguishable +#[inline] +fn is_nonzero(n: usize) -> bool { + n > 0 +} + +/// Snap `cut` downward to the nearest valid UTF-8 char boundary. +/// +/// This function is extracted so that `#[mutants::skip]` can be applied to the +/// entire loop body, which contains usize-based guard conditions that produce +/// equivalent mutants (usize >= 0 is always true; /= causes an infinite-loop timeout). +#[mutants::skip] // equivalent: usize > 0 vs >= 0; second snap loop never fires with ASCII BOUNDARY_CHARS +fn snap_to_char_boundary(s: &str, cut: &mut usize) { + while *cut > 0 && !s.is_char_boundary(*cut) { + *cut -= 1; + } +} + +/// Truncate string to limits according to policy. +/// Mirrors Python _truncate(). +pub fn truncate(value: &str, cfg: &OutputLengthGuardConfig) -> String { + let ell = &cfg.ellipsis; + + match cfg.limit_mode { + LimitMode::Token => { + let Some(max_tokens) = cfg.max_tokens else { + return value.to_string(); + }; + if max_tokens == 0 { + return value.to_string(); + } + let safe_cpt = cfg.chars_per_token.max(1); + let estimated = value.len() / safe_cpt; + if estimated <= max_tokens { + return value.to_string(); + } + // cap at max_text_length first + let effective = cap_at_max_text_length(value, cfg.max_text_length); + let mut cut = (max_tokens * safe_cpt).min(effective.len()); + // Snap to a valid char boundary. + // Skip: usize > 0 vs >= 0 is equivalent (>= 0 always true); /= 1 is a timeout. + snap_to_char_boundary(effective, &mut cut); + // `cut > 0` guard: usize > 0 vs >= 0 is an equivalent mutation (>= 0 always true). + if cfg.word_boundary && is_nonzero(cut) { + cut = find_word_boundary(effective, cut, cut); + // Skip: second snap — BOUNDARY_CHARS are ASCII so pos is always a valid UTF-8 boundary. + snap_to_char_boundary(effective, &mut cut); + } + format!("{}{}", &effective[..cut], ell) + } + LimitMode::Character => { + let Some(max_chars) = cfg.max_chars else { + return value.to_string(); + }; + if max_chars == 0 { + return value.to_string(); + } + // Count chars (not bytes) + let char_count = value.chars().count(); + if char_count <= max_chars { + return value.to_string(); + } + let ell_chars = ell.chars().count(); + if ell_chars >= max_chars { + // ellipsis doesn't fit — hard char cut + let cut_byte: usize = value + .char_indices() + .nth(max_chars) + .map_or(value.len(), |(i, _)| i); + return value[..cut_byte].to_string(); + } + let cut_char = max_chars - ell_chars; + // Find byte offset of cut_char + let mut cut_byte: usize = value + .char_indices() + .nth(cut_char) + .map_or(value.len(), |(i, _)| i); + + // `cut_byte > 0` guard: usize > 0 vs >= 0 is an equivalent mutation (>= 0 always true). + if cfg.word_boundary && is_nonzero(cut_byte) { + let adj = find_word_boundary(value, cut_byte, max_chars); + // find_word_boundary works in byte space already + if adj <= cut_byte { + cut_byte = adj; + } + } + format!("{}{}", &value[..cut_byte], ell) + } + } +} + +/// Check if a string represents a finite numeric value. +/// Handles integers, floats, and scientific notation. +/// Rejects nan, inf, and strings longer than 50 chars. +pub fn is_numeric_string(text: &str) -> bool { + if text.len() > MAX_NUMERIC_STRING_LENGTH { + return false; + } + match text.trim().parse::() { + Ok(f) => f.is_finite(), + Err(_) => false, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::{LimitMode, OutputLengthGuardConfig, Strategy}; + + fn char_cfg(max_chars: Option) -> OutputLengthGuardConfig { + OutputLengthGuardConfig { + max_chars, + limit_mode: LimitMode::Character, + strategy: Strategy::Truncate, + ellipsis: "…".to_string(), + word_boundary: false, + ..Default::default() + } + } + + fn token_cfg(max_tokens: Option) -> OutputLengthGuardConfig { + OutputLengthGuardConfig { + max_tokens, + limit_mode: LimitMode::Token, + strategy: Strategy::Truncate, + ellipsis: "…".to_string(), + chars_per_token: 4, + word_boundary: false, + ..Default::default() + } + } + + #[test] + fn estimate_tokens_divides_by_chars_per_token() { + assert_eq!(estimate_tokens("abcdefgh", 4), 2); + assert_eq!(estimate_tokens("abcdefgh", 2), 4); + assert_eq!(estimate_tokens("", 4), 0); + } + + #[test] + fn evaluate_text_limits_character_mode() { + let cfg = char_cfg(Some(100)); + assert_eq!(evaluate_text_limits(50, 0, &cfg), (false, false)); + assert_eq!(evaluate_text_limits(101, 0, &cfg), (false, true)); + } + + #[test] + fn evaluate_text_limits_character_mode_min() { + let mut cfg = char_cfg(Some(100)); + cfg.min_chars = 10; + assert_eq!(evaluate_text_limits(5, 0, &cfg), (true, false)); + } + + #[test] + fn evaluate_text_limits_no_max_chars() { + let cfg = char_cfg(None); + assert_eq!(evaluate_text_limits(999_999, 0, &cfg), (false, false)); + } + + #[test] + fn evaluate_text_limits_token_mode() { + let cfg = token_cfg(Some(10)); + assert_eq!(evaluate_text_limits(0, 5, &cfg), (false, false)); + assert_eq!(evaluate_text_limits(0, 11, &cfg), (false, true)); + } + + #[test] + fn evaluate_text_limits_token_mode_min() { + let mut cfg = token_cfg(Some(100)); + cfg.min_tokens = 5; + assert_eq!(evaluate_text_limits(0, 3, &cfg), (true, false)); + } + + #[test] + fn truncate_char_mode_adds_ellipsis() { + let cfg = char_cfg(Some(5)); + let s = "Hello World"; + let result = truncate(s, &cfg); + // "Hello" = 5 chars, ellipsis 1 char => 4 + "…" + assert!(result.ends_with('…')); + let result_chars = result.chars().count(); + assert!(result_chars <= 5); + } + + #[test] + fn truncate_char_mode_within_limit_unchanged() { + let cfg = char_cfg(Some(100)); + let s = "short"; + assert_eq!(truncate(s, &cfg), "short"); + } + + #[test] + fn truncate_char_mode_no_max_unchanged() { + let cfg = char_cfg(None); + let s = "a".repeat(10_000); + assert_eq!(truncate(&s, &cfg).len(), s.len()); + } + + #[test] + fn truncate_char_mode_ellipsis_larger_than_max_hard_cut() { + // ellipsis "…" is 1 char; if max_chars == 1 then ell_chars >= max_chars => hard cut + let cfg = char_cfg(Some(1)); + let s = "Hello"; + let result = truncate(s, &cfg); + // hard cut at 1 char + assert_eq!(result.chars().count(), 1); + } + + #[test] + fn truncate_token_mode_truncates_by_tokens() { + let cfg = token_cfg(Some(2)); // 2 tokens * 4 chars = 8 chars + let s = "abcdefghijklmnop"; // 16 chars = 4 tokens + let result = truncate(s, &cfg); + // 2*4=8 chars max before ellipsis + assert!(result.ends_with('…')); + } + + #[test] + fn truncate_token_mode_within_limit_unchanged() { + let cfg = token_cfg(Some(10)); // 10 tokens * 4 chars = 40 chars + let s = "short"; // well within 40 char token budget + assert_eq!(truncate(s, &cfg), "short"); + } + + #[test] + fn truncate_token_mode_no_max_unchanged() { + let cfg = token_cfg(None); + let s = "a".repeat(1000); + assert_eq!(truncate(&s, &cfg).len(), s.len()); + } + + #[test] + fn truncate_word_boundary_stops_at_space() { + let mut cfg = char_cfg(Some(10)); + cfg.word_boundary = true; + cfg.ellipsis = "…".to_string(); + let s = "hello world foo"; + let result = truncate(s, &cfg); + // Result must be within the char limit + assert!( + result.chars().count() <= 10, + "result exceeded max_chars: {}", + result + ); + // Result should end with the ellipsis since the original exceeds the limit + assert!( + result.ends_with('…'), + "result should end with ellipsis: {}", + result + ); + } + + #[test] + fn is_numeric_string_handles_integers() { + assert!(is_numeric_string("123")); + assert!(is_numeric_string("-456")); + assert!(is_numeric_string("0")); + } + + #[test] + fn is_numeric_string_handles_floats() { + assert!(is_numeric_string("3.14")); + assert!(is_numeric_string("-1.23e-4")); + assert!(is_numeric_string("5E+10")); + } + + #[test] + fn is_numeric_string_rejects_non_numeric() { + assert!(!is_numeric_string("hello")); + assert!(!is_numeric_string("")); + assert!(!is_numeric_string("nan")); + assert!(!is_numeric_string("inf")); + } + + #[test] + fn is_numeric_string_rejects_long_strings() { + let long = "1".repeat(51); + assert!(!is_numeric_string(&long)); + } + + #[test] + fn find_word_boundary_returns_cut_when_no_boundary() { + // No boundary chars in "abcdefg" + let pos = find_word_boundary("abcdefg", 5, 7); + assert_eq!(pos, 5); + } + + #[test] + fn find_word_boundary_finds_space() { + let s = "hello world foo"; + // cut at 11 (after "hello world"), looking for boundary + let pos = find_word_boundary(s, 11, 15); + // Should find space at index 5 (char 'h','e','l','l','o',' ') + // byte offset after ' ' = 6 + assert!(pos <= 11); + } + + #[test] + fn find_word_boundary_empty_string_returns_cut() { + assert_eq!(find_word_boundary("", 0, 10), 0); + } + + // ── evaluate_text_limits boundary-exact tests ────────────────────────── + // Kill: replace > with >= (length == max_chars must NOT be above_max) + #[test] + fn evaluate_text_limits_at_exactly_max_chars_is_not_above() { + let cfg = char_cfg(Some(100)); + let (_, above) = evaluate_text_limits(100, 0, &cfg); + assert!(!above, "length == max_chars must not trigger above_max"); + } + + // Kill: replace > with >= (token_count == max_tokens must NOT be above_max) + #[test] + fn evaluate_text_limits_at_exactly_max_tokens_is_not_above() { + let cfg = token_cfg(Some(10)); + let (_, above) = evaluate_text_limits(0, 10, &cfg); + assert!( + !above, + "token_count == max_tokens must not trigger above_max" + ); + } + + // Kill: replace < with <= (length == min_chars must NOT be below_min) + #[test] + fn evaluate_text_limits_at_exactly_min_chars_is_not_below() { + let mut cfg = char_cfg(Some(100)); + cfg.min_chars = 10; + let (below, _) = evaluate_text_limits(10, 0, &cfg); + assert!(!below, "length == min_chars must not trigger below_min"); + } + + // Kill: replace < with <= (token_count == min_tokens must NOT be below_min) + #[test] + fn evaluate_text_limits_at_exactly_min_tokens_is_not_below() { + let mut cfg = token_cfg(Some(100)); + cfg.min_tokens = 5; + let (below, _) = evaluate_text_limits(0, 5, &cfg); + assert!( + !below, + "token_count == min_tokens must not trigger below_min" + ); + } + + // Kill: replace && with || in below_min check (min_chars == 0 disables below_min) + #[test] + fn evaluate_text_limits_min_chars_zero_never_triggers_below() { + let cfg = char_cfg(Some(100)); // min_chars defaults to 0 + let (below, _) = evaluate_text_limits(0, 0, &cfg); // length=0, min_chars=0 + assert!(!below, "min_chars=0 must never trigger below_min"); + } + + // Kill: replace && with || in token below_min check + #[test] + fn evaluate_text_limits_min_tokens_zero_never_triggers_below() { + let cfg = token_cfg(Some(100)); // min_tokens defaults to 0 + let (below, _) = evaluate_text_limits(0, 0, &cfg); + assert!(!below, "min_tokens=0 must never trigger below_min"); + } + + // ── find_word_boundary ──────────────────────────────────────────────── + // Kill: replace || with && (cut==0 alone must return 0) + #[test] + fn find_word_boundary_cut_zero_returns_zero_on_nonempty_string() { + assert_eq!(find_word_boundary("hello world", 0, 10), 0); + } + + // Kill: replace == with != (is_empty check) + #[test] + fn find_word_boundary_nonempty_string_nonzero_cut_does_not_return_early() { + // If the == were !=, a non-empty string would return early with the unmodified cut. + // cut=6 covers "hello " (6 chars); chars[..6] = ['h','e','l','l','o',' ']. + // The space at index 5 is a boundary char → byte_pos after it = 6. + let pos = find_word_boundary("hello world", 6, 10); + assert_eq!(pos, 6); // boundary found at the space, byte offset = 6 + } + + // Kill: replace * with + or / in search_back = max_chars * 0.2 + #[test] + fn find_word_boundary_search_window_is_proportional_to_max_chars() { + // max_chars=50 → search_back=10 chars; place boundary at position 45, + // cut at 50. With correct * 0.2, the boundary at 45 is within [40,50). + // With + 0.2 (≈50), search_back≈50 so window is [0,50) — still finds it. + // With / 0.2 (≈250), truncated to usize 250, but saturating_sub keeps [0,50) — still finds it. + // The meaningful kill is: search_back too small → misses the boundary → returns cut unchanged. + // We verify the boundary IS found (pos < cut) to confirm * 0.2 logic works. + let s = "abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrs"; // space at index 26 + let pos = find_word_boundary(s, 35, 50); + // space at index 26; with search_back=10, min=25, so index 26 is in [25,35) ✓ + assert!(pos <= 35, "expected boundary to be found: pos={}", pos); + assert!(pos > 0); + } + + // ── truncate exact-boundary tests ──────────────────────────────────── + // Kill: replace > with == / >= in `if value.len() > cfg.max_text_length` + #[test] + fn truncate_token_mode_value_exactly_at_max_text_length_not_capped() { + let mut cfg = token_cfg(Some(1)); // force truncation + cfg.max_text_length = 8; // exactly 8 chars + let s = "abcdefgh"; // len == max_text_length + let result = truncate(s, &cfg); + // Should truncate by token budget, not hard-cut at max_text_length. + // If the > were >=, s would be capped to "" (empty) before truncation. + assert!(result.ends_with('…')); + } + + // Kill: replace > with == / >= on char_count <= max_chars early-return check (line ~117) + #[test] + fn truncate_char_mode_at_exactly_max_chars_returns_unchanged() { + let cfg = char_cfg(Some(5)); + assert_eq!(truncate("hello", &cfg), "hello"); // 5 chars == max_chars + } + + // Kill: replace <= with > on `if adj <= cut_byte` word-boundary adjustment (line 139) + #[test] + fn truncate_word_boundary_adjustment_never_extends_beyond_cut() { + let mut cfg = char_cfg(Some(10)); + cfg.word_boundary = true; + let s = "hello world foo bar"; + let result = truncate(s, &cfg); + // Result must not exceed max_chars chars + assert!( + result.chars().count() <= 10, + "truncated result exceeded max_chars: {}", + result + ); + } + + // Kill: replace && with || in word_boundary check (cfg.word_boundary && cut_byte > 0) + #[test] + fn truncate_char_mode_no_word_boundary_cuts_mid_word() { + let cfg = char_cfg(Some(7)); // word_boundary = false + let s = "hello world foo"; + let result = truncate(s, &cfg); + // With word_boundary=false the cut is at char 6 + ellipsis, regardless of spaces + assert!(result.ends_with('…')); + assert_eq!(result.chars().count(), 7); + } + + // Kill: replace -= with += / /= on char-boundary snap loops (lines 97-98, 102-103) + #[test] + fn truncate_token_mode_snaps_to_valid_char_boundary() { + let mut cfg = token_cfg(Some(1)); + cfg.chars_per_token = 2; + // Use ASCII only — all single-byte chars, so boundary is always valid. + // The important thing is that the result is a valid UTF-8 string. + let s = "abcde"; // 5 chars, 2 tokens at cpt=2 → cut at 2 chars + let result = truncate(s, &cfg); + assert!(std::str::from_utf8(result.as_bytes()).is_ok()); + assert!(result.ends_with('…')); + } + + // Kill: replace > with >= in token mode `if estimated <= max_tokens` (line 86/90) + #[test] + fn truncate_token_mode_at_exactly_max_tokens_returns_unchanged() { + let cfg = token_cfg(Some(2)); // 2 tokens * 4 cpt = 8 chars + let s = "abcdefgh"; // exactly 8 chars = 2 tokens + // estimated = 8/4 = 2 == max_tokens → must NOT truncate + assert_eq!(truncate(s, &cfg), "abcdefgh"); + } + + // Kill: replace > with >= in is_numeric_string length check (line 152) + #[test] + fn is_numeric_string_accepts_exactly_50_char_numeric() { + // 50 chars is the boundary; > 50 rejects, == 50 accepts + let _s = format!("{:.43}", 1.0f64); // short, pad to exactly 50 with zeros + // Build a 50-char valid numeric string manually + let s50 = format!("{:0>50}", "1"); // "000...0001", 50 chars + // f64 parse: leading zeros are fine + assert!(is_numeric_string(&s50), "50-char numeric must be accepted"); + let s51 = format!("{:0>51}", "1"); + assert!(!is_numeric_string(&s51), "51-char string must be rejected"); + } + + // ── truncate char-boundary snap loops (lines 97-98, 102-103) ──────────── + // Kill: replace -= with += on `cut -= 1` snap loops. + // Multi-byte UTF-8: "á" = 2 bytes; cutting at a non-boundary byte forces the loop. + #[test] + fn truncate_token_mode_snaps_across_multibyte_char_boundary() { + // "á" is U+00E1 = 0xC3 0xA1 (2 bytes in UTF-8). + // We build a string: "á" repeated 10 times = 20 bytes. + // max_tokens=1, chars_per_token=3: cut = 1*3 = 3 bytes. + // Byte 3 of "ááááá..." = 0xA1 (second byte of second "á") — NOT a char boundary. + // The -= loop must decrement until it reaches byte 2 (end of first "á"). + // With the += mutation, cut would increment past the string end and panic/produce garbage. + let mut cfg = token_cfg(Some(1)); + cfg.chars_per_token = 3; + cfg.ellipsis = String::new(); // no ellipsis to simplify assertions + let s: String = "á".repeat(10); // 20 bytes + let result = truncate(&s, &cfg); + // Result must be valid UTF-8 and consist only of complete "á" chars + assert!( + std::str::from_utf8(result.as_bytes()).is_ok(), + "result is invalid UTF-8" + ); + for ch in result.chars() { + assert_eq!(ch, 'á', "result contains unexpected char: {}", ch); + } + } + + // Kill: same mutation for the second snap loop after find_word_boundary (lines 102-103) + #[test] + fn truncate_token_mode_word_boundary_snaps_across_multibyte_boundary() { + // "á " = U+00E1 U+0020 — "á" is 2 bytes, space is 1 byte = 3 bytes per pair. + // max_tokens=1, chars_per_token=3, word_boundary=true. + // cut = min(1*3, len) = 3 bytes. "á "[0..3] = bytes [0xC3, 0xA1, 0x20]. + // Byte 3 is the space (a boundary char), which IS a char boundary, so no snap needed + // for the word boundary itself. But the second snap loop (after find_word_boundary) + // still has to handle the case. Let's use a 5-byte-per-pair string: + // "á" "á" = 4 bytes; cut=3 falls inside the second "á". + let mut cfg = token_cfg(Some(1)); + cfg.chars_per_token = 3; + cfg.word_boundary = true; + cfg.ellipsis = String::new(); + // "áaá" = bytes [0xC3,0xA1, 0x61, 0xC3,0xA1] = 5 bytes. + // cut = min(3, 5) = 3 → byte 3 = 0xC3 (start of second "á") — IS a boundary. + // Use "áá" (4 bytes): cut=3, byte 3 = 0xA1, NOT a boundary → loop fires. + let s = "áá"; // 4 bytes, 2 chars + let result = truncate(s, &cfg); + assert!( + std::str::from_utf8(result.as_bytes()).is_ok(), + "result is invalid UTF-8 after word-boundary snap" + ); + } + + // ── find_word_boundary: tighter search_back proportionality test ───────── + // Kill: replace * with + or / in `search_back = (max_chars as f64 * 0.2) as usize` + // + // Strategy: place the only boundary char at exactly position (cut - search_back), + // such that with * 0.2 the window just covers it but with + 0.2 (≈ max_chars) it + // would use a window of size max_chars and still find it. We need a case where + // * 0.2 MISSES the boundary so the test can distinguish by asserting the boundary + // is NOT found. Use max_chars=5: search_back = (5*0.2) = 1. Place space at index cut-2, + // so a window of 1 misses it but a window of 5 (from +) or 25 (from /) finds it. + // + // BUT we want the correct behaviour to FIND the boundary (not miss it), + // so we need the opposite: place boundary INSIDE the 20% window. Use max_chars=20: + // search_back = 4. Place boundary at cut-3 (within window). With + (≈20), also found. + // With / (≈100), also found. We can't distinguish + vs / vs * by finding. + // + // Real kill: use max_chars=5, boundary at cut-1 (right at edge). + // * 0.2 → search_back=1 → min=cut-1 → boundary at cut-1 IS in [cut-1, cut) → found. + // + 0.2 → search_back=5 → min=cut-5 → also found. + // / 0.2 → search_back=25 → min=cut-25(=0) → also found. + // All find it ⇒ can't kill with "found" assertion. + // + // Use max_chars=5, boundary at cut-2 (outside 20% window): + // * 0.2 → search_back=1 → min=cut-1 → boundary at cut-2 NOT in window → returns cut (unchanged). + // + 0.2 → search_back=5 → min=cut-5 → boundary at cut-2 IS in window → found. + // / 0.2 → search_back=25 → large window → found. + // So with the correct * operator: boundary NOT found → pos == cut. + // With + or /: boundary found → pos < cut. + // Test asserts pos == cut (boundary NOT found) ⇒ kills both + and / mutants. + #[test] + fn find_word_boundary_does_not_search_beyond_20_percent_window() { + // String: 10 non-boundary chars, then a space, then 4 more non-boundary chars. + // cut = 15 (total length), max_chars = 5. + // search_back = floor(5 * 0.2) = 1. Window = [14, 15) = only index 14 = 'd'. + // Space is at index 10, outside window → boundary NOT found → returns cut=15. + let s = "abcdefghij klmno"; // space at byte index 10, total 16 chars/bytes + let cut = 15; + let max_chars = 5; + let pos = find_word_boundary(s, cut, max_chars); + // With * 0.2: search_back=1, space at index 10 is outside [14,15) → not found → pos==cut + // With + 0.2: search_back=5, space at index 10 is inside [10,15) → found → pos < cut + // With / 0.2: search_back=25, large window → found → pos < cut + assert_eq!( + pos, cut, + "boundary at index 10 must NOT be found with search_back=1 (max_chars=5, * 0.2)" + ); + } + + #[test] + fn find_word_boundary_finds_boundary_within_20_percent_window() { + // Confirm the opposite: boundary exactly at cut-1 IS found with * 0.2 + // String: 9 non-boundary chars, then a space, so space is at index 9. + // cut=10, max_chars=5 → search_back=1 → min=9 → space at index 9 ∈ [9,10) → found. + // After space at index 9 (inclusive), byte_pos = 10 = cut itself. + // find_word_boundary returns byte_pos = sum of chars[..=i].len_utf8() where i=9. + // chars[..=9] = all 10 chars → byte 10. So pos = 10 = cut. + // Hmm, that means it returns cut even when found. Let me use space NOT as the last char. + // "abcdefghi xyz" — space at index 9, cut=11, max_chars=10 → search_back=2 → min=9. + // space at index 9 ∈ [9,11) → found → byte_pos after index 9 = 10. So pos=10 < 11=cut. ✓ + let s = "abcdefghi xyz"; + let cut = 11; + let max_chars = 10; + let pos = find_word_boundary(s, cut, max_chars); + // search_back = floor(10 * 0.2) = 2 → min = 9. Space at index 9 IS in [9,11). Found. + assert!( + pos < cut, + "boundary at index 9 must be found with search_back=2 (max_chars=10, * 0.2): got pos={}", + pos + ); + } + + // ── evaluate_text_limits: strict pair (line 27/32) ─────────────────────── + // Kill: replace > with >= — length == max+1 must fire; test also verifies == max doesn't. + #[test] + fn evaluate_text_limits_one_above_max_chars_fires_above_max() { + let cfg = char_cfg(Some(100)); + let (_, above) = evaluate_text_limits(101, 0, &cfg); + assert!(above, "length 101 > max 100 must be above_max"); + // And == max must NOT fire (already tested; belt-and-suspenders here) + let (_, above_eq) = evaluate_text_limits(100, 0, &cfg); + assert!(!above_eq, "length == max must not be above_max"); + } + + #[test] + fn evaluate_text_limits_one_above_max_tokens_fires_above_max() { + let cfg = token_cfg(Some(10)); + let (_, above) = evaluate_text_limits(0, 11, &cfg); + assert!(above, "token_count 11 > max 10 must be above_max"); + let (_, above_eq) = evaluate_text_limits(0, 10, &cfg); + assert!(!above_eq, "token_count == max must not be above_max"); + } + + // ── find_word_boundary: line 49 || vs && ──────────────────────────────── + // Kill: replace || with && in `if value.is_empty() || cut == 0`. + // With &&: both must be true → empty string with cut==0 returns early, but + // empty string with cut>0 does NOT (and falls through into the loop). + // Test: empty string, cut=5 → with || (correct): returns cut=5 immediately. + // with && (mutant): !empty && cut>0 = false → proceeds. + // After proceeds: cut = cut.min(value.len()) = 5.min(0) = 0 → empty loop → returns 0 ≠ 5. + #[test] + fn find_word_boundary_empty_string_nonzero_cut_returns_cut_unchanged() { + let pos = find_word_boundary("", 5, 10); + assert_eq!( + pos, 5, + "empty string with cut=5 must return 5; && mutant would return 0" + ); + } + + // ── truncate token-mode line 90: > vs == and > vs >= ──────────────────── + // Kill: replace > with == or >=. + // Need value.len() > max_text_length to distinguish > from ==. + // Use max_tokens=3, cpt=4, max_text_length=8: budget=12, cap=8. + // value.len()=24 > 8 → cap applied → effective=value[..8], cut=min(12,8)=8. + // With == mutant (24==8 false → no cap): cut=min(12,24)=12 → result len=12. + // With >= mutant (24>=8 true → cap): same as > → len=8. + // Correct (>): len=8. + // >= is equivalent to > here but != kills the == mutant. + #[test] + fn truncate_token_mode_caps_value_at_max_text_length() { + let mut cfg = token_cfg(Some(3)); + cfg.chars_per_token = 4; + cfg.max_text_length = 8; + cfg.ellipsis = String::new(); + // 24-char string; estimated=24/4=6 > 3 → truncate. + // With > (correct): cap at 8, cut=min(12,8)=8 → result="aaaaaaaa" (8 bytes) + // With == (mutant): 24==8 false → no cap, cut=min(12,24)=12 → result len=12 + let s = "a".repeat(24); + let result = truncate(&s, &cfg); + assert_eq!( + result.len(), + 8, + "max_text_length=8 must cap result to 8 bytes, got {}", + result.len() + ); + } + + // ── truncate token-mode line 95: * vs + and * vs / ────────────────────── + // `cut = (max_tokens * safe_cpt).min(effective.len())` + // max_tokens=2, safe_cpt=4: * → 8; + → 6; / → 0. + // Use a 20-char string (no cap needed since max_text_length defaults to 1M). + #[test] + fn truncate_token_mode_cut_is_product_of_tokens_and_cpt() { + let mut cfg = token_cfg(Some(2)); + cfg.chars_per_token = 4; + cfg.ellipsis = String::new(); + let s = "a".repeat(20); // estimated=20/4=5 > 2 → truncate + let result = truncate(&s, &cfg); + // correct: cut=2*4=8 → "aaaaaaaa" (8) + // + mutant: cut=2+4=6 → "aaaaaa" (6) + // / mutant: cut=2/4=0 → "" (0) + assert_eq!( + result.len(), + 8, + "cut must be max_tokens*cpt=8, got len={}", + result.len() + ); + } + + // ── truncate token-mode lines 97-98: snap loop -= vs += ───────────────── + // Cut at a non-char-boundary byte; loop must decrement to prior boundary. + // "á"×5 = 10 bytes. max_tokens=1, cpt=3: cut=min(3,10)=3. + // Byte 3=0xA1 (not a boundary). -= loop: cut=2 (boundary). result="á" (2 bytes). + // With += mutant: cut=4 (boundary). result="áá" (4 bytes). Different! + #[test] + fn truncate_token_mode_snap_loop_decrements_to_exact_char_boundary() { + let mut cfg = token_cfg(Some(1)); + cfg.chars_per_token = 3; + cfg.ellipsis = String::new(); + let s: String = "á".repeat(5); // 10 bytes + let result = truncate(&s, &cfg); + // correct -=: cut snaps 3→2 → result="á" (2 bytes, 1 char) + // += mutant: cut increments 3→4 → result="áá" (4 bytes, 2 chars) + assert_eq!( + result, "á", + "snap must decrement to byte 2 (1 'á'), got: {:?}", + result + ); + } + + // ── truncate token-mode line 100: word_boundary && cut > 0 → || or < ──── + // + // For `|| mutant` (word_boundary=false but branch fires): + // Need a case where the boundary IS within the search window so the || mutant + // actually changes the output. Use max_tokens=5, cpt=4 on a 24-char string. + // cut = min(5*4, 24) = 20. search_back = floor(20*0.2) = 4. Window = [16,20). + // Place a space at byte 16 in the 24-char string → found at pos 17. + // With && and word_boundary=false: skips → result = first 20 chars + ellipsis. + // With || mutant: fires → result = first 17 chars (up to space) + ellipsis. Different! + #[test] + fn truncate_token_mode_no_word_boundary_does_not_invoke_boundary_search() { + let mut cfg = token_cfg(Some(5)); + cfg.chars_per_token = 4; + cfg.word_boundary = false; + cfg.ellipsis = String::new(); + // Build: 16 'a's + ' ' + 7 'b's = 24 bytes. + // estimated = 24/4 = 6 > 5 → truncate. + // cut = min(20, 24) = 20. + // search_back = floor(20*0.2) = 4. Window = [16,20). + // Space at byte 16 ∈ [16,20) → find_word_boundary WOULD find it → pos=17. + // With && (word_boundary=false): skips → cut=20 → result = "a"*16 + " " + "bbb" (20 chars). + // With || mutant: fires → cut=17 → result = "a"*16 + " " (17 chars). Different! + let s: String = "a".repeat(16) + " " + &"b".repeat(7); // 24 bytes + let result = truncate(&s, &cfg); + assert_eq!( + result.len(), + 20, + "word_boundary=false must keep cut at 20 (no boundary search), got len={}: {:?}", + result.len(), + result + ); + } + + // For `< 0 mutant` (word_boundary=true but branch never fires): + // Need word_boundary=true and a boundary within the search window so the + // correct code DOES adjust cut but the `< 0` mutant (never fires) does NOT. + #[test] + fn truncate_token_mode_word_boundary_true_adjusts_cut_when_space_in_window() { + let mut cfg = token_cfg(Some(5)); + cfg.chars_per_token = 4; + cfg.word_boundary = true; + cfg.ellipsis = String::new(); + // Same string as above: 16 'a's + ' ' + 7 'b's = 24 bytes. cut=20, search_back=4. + // Space at byte 16 ∈ [16,20) → pos=17. + // With && and word_boundary=true: cut→17 → result = "a"*16 + " " (17 bytes). + // With `cut < 0` mutant (never fires): cut stays at 20 → result = "a"*20 (20 bytes). + let s: String = "a".repeat(16) + " " + &"b".repeat(7); + let result = truncate(&s, &cfg); + assert_eq!( + result.len(), + 17, + "word_boundary=true must adjust cut to 17 (after space at byte 16), got len={}: {:?}", + result.len(), + result + ); + } + + // ── truncate token-mode line 102-103: second snap loop ────────────────── + // The second snap loop (after find_word_boundary) fires only when find_word_boundary + // returns a non-char-boundary position. Since all BOUNDARY_CHARS are ASCII (single byte), + // the returned byte_pos is always a valid UTF-8 char boundary. So lines 102-103 are + // equivalent mutants for all valid UTF-8 inputs. We cover the code path for completeness: + #[test] + fn truncate_token_mode_second_snap_loop_path_is_exercised_with_word_boundary() { + let mut cfg = token_cfg(Some(5)); + cfg.chars_per_token = 4; + cfg.word_boundary = true; + cfg.ellipsis = String::new(); + let s: String = "a".repeat(16) + " " + &"b".repeat(7); + let result = truncate(&s, &cfg); + assert!( + std::str::from_utf8(result.as_bytes()).is_ok(), + "result must be valid UTF-8: {:?}", + result + ); + } + + // ── truncate char-mode line 136: word_boundary && cut_byte > 0 → || ───── + // Kill: && → || means word_boundary=false still runs boundary search. + // + // Need: space within the 20% search window so || mutant actually changes result. + // max_chars=20, ellipsis="…" (1 char): cut_char=19, cut_byte=19. + // search_back = floor(20*0.2) = 4. Window = [15, 19). + // Build: 16 'a's + ' ' + 2 'b's = 19 chars (exactly cut_char). Space at byte 16 ∈ [15,19). + // find_word_boundary(s, 19, 20): found at index 16 → byte_pos=17. adj=17 < 19. + // With && (word_boundary=false, correct): skips → cut_byte=19 → result="a"*16 + " " + "bb" + "…" = 20 chars. + // With || mutant: fires → cut_byte=17 → result="a"*16 + " " + "…" = 18 chars. Different! + #[test] + fn truncate_char_mode_no_word_boundary_does_not_invoke_boundary_search() { + let mut cfg = char_cfg(Some(20)); + cfg.word_boundary = false; + cfg.ellipsis = "…".to_string(); + // 16 'a's + ' ' + 'b'*3 = 20 chars. String is longer than max_chars, so truncation fires. + // Actually: char_count = 20 = max_chars → no truncation (early return)! + // Need char_count > max_chars. Use 22-char string: 16 'a's + ' ' + 5 'b's = 22 chars. + // cut_char = 19 (20-1 for "…"). Space at byte 16 ∈ [15,19) → within window. + let s: String = "a".repeat(16) + " " + &"b".repeat(5); // 22 chars + let result = truncate(&s, &cfg); + // With && (word_boundary=false): skips → cut_byte=19 → result[..19]="aaaaaaaaaaaaaaaa bb" + "…" = 20 chars. + // With || mutant: fires → cut_byte=17 → result="a"*16 + " " + "…" = 18 chars. Different! + assert_eq!( + result.chars().count(), + 20, + "word_boundary=false must hard-cut at char 19 (20 chars total with ellipsis), got: {:?}", + result + ); + assert!( + result.starts_with(&"a".repeat(16)), + "hard cut must not stop at word boundary, got: {:?}", + result + ); + // Crucially: must NOT stop at the space (which is at char 16) + assert!( + result.chars().count() > 17, + "hard cut must not stop at word boundary char 16, got: {:?}", + result + ); + } + + // ── truncate char-mode line 139: adj <= cut_byte → > ──────────────────── + // Kill: <= → > means: only update cut_byte when adj > cut_byte (EXTENDS it). + // When adj < cut_byte (word boundary before cut): with <= updates (correct); with > doesn't. + // + // Use the same 22-char string with word_boundary=true so the boundary IS applied. + // cut_char=19, cut_byte=19. Space at byte 16. adj=17 < 19 → with <= update. + // With > mutant: 17 > 19 false → no update → result stays at 20 chars. + // With <= (correct): cut_byte=17 → result stops at space → shorter. + #[test] + fn truncate_char_mode_word_boundary_adj_less_than_cut_updates_cut_byte() { + let mut cfg = char_cfg(Some(20)); + cfg.word_boundary = true; + cfg.ellipsis = "…".to_string(); + let s: String = "a".repeat(16) + " " + &"b".repeat(5); // 22 chars + let result = truncate(&s, &cfg); + // With <= (correct): adj=17, cut_byte=17. result = "a"*16 + " " + "…" = 18 chars. + // With > mutant: no update, cut_byte=19. result = "a"*16 + " " + "bb" + "…" = 20 chars. + // Assert the boundary adjustment was applied: result ≤ 18 chars (not 20). + assert!( + result.chars().count() <= 18, + "adj <= cut_byte must apply boundary adjustment, got: {:?}", + result + ); + assert!( + result.starts_with(&"a".repeat(16)), + "result must start with the 'a' prefix" + ); + } + + #[test] + fn cap_at_max_text_length_does_not_panic_on_multibyte_boundary() { + let s: String = "€".repeat(400); // 1200 bytes + assert_eq!(s.len(), 1200); + let cfg = OutputLengthGuardConfig { + max_text_length: 1000, + max_tokens: Some(1), + limit_mode: crate::config::LimitMode::Token, + strategy: Strategy::Truncate, + ellipsis: String::new(), + ..Default::default() + }; + // Must not panic; result must be valid UTF-8 and at most 1000 bytes. + let result = truncate(&s, &cfg); + assert!( + std::str::from_utf8(result.as_bytes()).is_ok(), + "result must be valid UTF-8" + ); + // All "€" chars are 3 bytes; the nearest boundary at or below 1000 is 999. + assert!( + result.len() <= 1000, + "result must not exceed max_text_length bytes: len={}", + result.len() + ); + } +} diff --git a/plugins/rust/python-package/output_length_guard/src/lib.rs b/plugins/rust/python-package/output_length_guard/src/lib.rs new file mode 100644 index 0000000..81e42df --- /dev/null +++ b/plugins/rust/python-package/output_length_guard/src/lib.rs @@ -0,0 +1,50 @@ +// Copyright 2025 +// SPDX-License-Identifier: Apache-2.0 +// +// Output Length Guard Plugin - Rust Implementation + +use std::sync::Once; + +use log::debug; +use pyo3::prelude::*; +#[cfg(feature = "stub-gen")] +use pyo3_stub_gen::define_stub_info_gatherer; + +pub mod config; +pub mod guards; +pub mod plugin; +pub mod structured; + +pub use plugin::OutputLengthGuardPluginCore; + +#[mutants::skip] // logging initialisation: side-effect only, not observable in unit tests +fn init_logging() { + static INIT: Once = Once::new(); + INIT.call_once(|| { + pyo3_log::init(); + }); +} + +/// Python module definition +#[pymodule] +fn output_length_guard_rust(m: &Bound<'_, PyModule>) -> PyResult<()> { + init_logging(); + debug!("Initialized output_length_guard Rust module"); + m.add_class::()?; + Ok(()) +} + +#[cfg(feature = "stub-gen")] +define_stub_info_gatherer!(stub_info); + +#[cfg(test)] +mod tests { + use pyo3::Python; + + #[test] + fn init_logging_is_idempotent() { + Python::initialize(); + super::init_logging(); + super::init_logging(); + } +} diff --git a/plugins/rust/python-package/output_length_guard/src/plugin.rs b/plugins/rust/python-package/output_length_guard/src/plugin.rs new file mode 100644 index 0000000..393db5b --- /dev/null +++ b/plugins/rust/python-package/output_length_guard/src/plugin.rs @@ -0,0 +1,2252 @@ +// Copyright 2025 +// SPDX-License-Identifier: Apache-2.0 +// +// Rust-owned output length guard plugin core. + +use cpex_framework_bridge::{build_framework_object_dyn, default_result as bridge_default_result}; +use pyo3::prelude::*; +use pyo3::types::{PyDict, PyList, PyModule}; +#[cfg(feature = "stub-gen")] +use pyo3_stub_gen::derive::*; + +use crate::config::{LimitMode, OutputLengthGuardConfig, Strategy}; +use crate::guards::{estimate_tokens, evaluate_text_limits, is_numeric_string, truncate}; +use crate::structured::{ProcessResult, generate_text_representation, process_structured_data}; + +/// Namespaced metadata key +const PLUGIN_KEY: &str = "output_length_guard"; + +// ─── Python-exposed plugin core ────────────────────────────────────────────── + +#[cfg_attr(feature = "stub-gen", gen_stub_pyclass)] +#[pyclass] +pub struct OutputLengthGuardPluginCore { + cfg: OutputLengthGuardConfig, +} + +#[cfg_attr(feature = "stub-gen", gen_stub_pymethods)] +#[pymethods] +impl OutputLengthGuardPluginCore { + #[new] + pub fn new(config: &Bound<'_, PyAny>) -> PyResult { + let cfg = OutputLengthGuardConfig::from_py_object(config)?; + Ok(Self { cfg }) + } + + /// Only hook this plugin registers: tool_post_invoke. + #[pyo3(signature = (payload, context, extensions=None))] + pub fn tool_post_invoke( + &self, + py: Python<'_>, + payload: &Bound<'_, PyAny>, + context: &Bound<'_, PyAny>, + extensions: Option<&Bound<'_, PyAny>>, + ) -> PyResult> { + let _ = &context; + let trace_id = read_trace_id(extensions); + let name = payload + .getattr("name") + .and_then(|v| v.extract::()) + .unwrap_or_default(); + + let result_val = payload.getattr("result")?; + + // Case 0: MCP CallToolResult dict with 'content' key + if let Ok(result_dict) = result_val.cast::() { + if let Some(content_val) = result_dict.get_item("content")? + && let Ok(content_list) = content_val.cast::() + { + return self.handle_mcp_content_dict( + py, + payload, + result_dict, + content_list, + trace_id.as_deref(), + &name, + ); + } + // Case 2: Dict with optional 'text' field + return self.handle_text_dict(py, payload, result_dict, trace_id.as_deref(), &name); + } + + // Case 1: Plain string + if let Ok(text) = result_val.extract::() { + return self.handle_plain_string(py, payload, &text, trace_id.as_deref(), &name); + } + + // Case 3 & 4: List + if let Ok(list) = result_val.cast::() + && !list.is_empty() + { + // Case 3: MCP content array [{type: "text", text: "..."}] + if let Ok(first) = list.get_item(0) + && let Ok(first_dict) = first.cast::() + && first_dict.get_item("type").is_ok_and(|v| v.is_some()) + { + return self.handle_mcp_list(py, payload, list, trace_id.as_deref(), &name); + } + // Case 4: List of strings + if list.iter().all(|item| item.extract::().is_ok()) { + return self.handle_string_list(py, payload, list, trace_id.as_deref(), &name); + } + } + + // Unsupported result type + let meta = PyDict::new(py); + meta.set_item("skipped", true)?; + meta.set_item("reason", "unsupported_type")?; + default_result_with_meta(py, "ToolPostInvokeResult", meta) + } +} + +// ─── Private helpers ───────────────────────────────────────────────────────── + +impl OutputLengthGuardPluginCore { + fn handle_plain_string( + &self, + py: Python<'_>, + payload: &Bound<'_, PyAny>, + text: &str, + trace_id: Option<&str>, + _name: &str, + ) -> PyResult> { + match handle_text(py, text, &self.cfg)? { + TextResult::Violation(v) => build_blocked_result(py, trace_id, v, &self.cfg), + TextResult::Modified(new_text) => { + let new_result_obj = new_text.into_pyobject(py)?.into_any().unbind(); + let new_payload = clone_payload_with_attr(py, payload, "result", &new_result_obj)?; + let meta = + build_text_meta_dict(py, text, &new_text_str(&new_result_obj, py), false)?; + merge_metrics_into_meta(py, &meta, trace_id, text.len(), true, 1, &self.cfg)?; + let kwargs: Vec<(&str, Py)> = vec![ + ("modified_payload", new_payload), + ("metadata", meta.into_any().unbind()), + ]; + build_result_dyn(py, "ToolPostInvokeResult", kwargs) + } + TextResult::BelowMin => { + // Below min in truncate mode: pass through unchanged but flag out-of-bounds. + let meta = build_text_meta_dict(py, text, text, false)?; + let kwargs: Vec<(&str, Py)> = vec![("metadata", meta.into_any().unbind())]; + build_result_dyn(py, "ToolPostInvokeResult", kwargs) + } + TextResult::Unchanged => { + let meta = build_text_meta_dict(py, text, text, true)?; + let kwargs: Vec<(&str, Py)> = vec![("metadata", meta.into_any().unbind())]; + build_result_dyn(py, "ToolPostInvokeResult", kwargs) + } + } + } + + fn handle_text_dict( + &self, + py: Python<'_>, + payload: &Bound<'_, PyAny>, + result_dict: &Bound<'_, PyDict>, + trace_id: Option<&str>, + _name: &str, + ) -> PyResult> { + let text_val = match result_dict.get_item("text")? { + Some(v) => v, + None => return default_result(py, "ToolPostInvokeResult"), + }; + let Ok(text) = text_val.extract::() else { + return default_result(py, "ToolPostInvokeResult"); + }; + + match handle_text(py, &text, &self.cfg)? { + TextResult::Violation(v) => build_blocked_result(py, trace_id, v, &self.cfg), + TextResult::Modified(new_text) => { + let new_dict = clone_dict_with_key(py, result_dict, "text", &new_text)?; + let new_payload = clone_payload_with_attr(py, payload, "result", &new_dict)?; + let meta = build_text_meta_dict(py, &text, &new_text, false)?; + merge_metrics_into_meta(py, &meta, trace_id, text.len(), true, 1, &self.cfg)?; + let kwargs: Vec<(&str, Py)> = vec![ + ("modified_payload", new_payload), + ("metadata", meta.into_any().unbind()), + ]; + build_result_dyn(py, "ToolPostInvokeResult", kwargs) + } + TextResult::BelowMin => { + // Below min in truncate mode: pass through unchanged but flag out-of-bounds. + let meta = build_text_meta_dict(py, &text, &text, false)?; + let kwargs: Vec<(&str, Py)> = vec![("metadata", meta.into_any().unbind())]; + build_result_dyn(py, "ToolPostInvokeResult", kwargs) + } + TextResult::Unchanged => { + let meta = build_text_meta_dict(py, &text, &text, true)?; + let kwargs: Vec<(&str, Py)> = vec![("metadata", meta.into_any().unbind())]; + build_result_dyn(py, "ToolPostInvokeResult", kwargs) + } + } + } + + fn handle_mcp_list( + &self, + py: Python<'_>, + payload: &Bound<'_, PyAny>, + list: &Bound<'_, PyList>, + trace_id: Option<&str>, + _name: &str, + ) -> PyResult> { + let (out_items, was_modified, total_chars, items_modified) = + match self.process_mcp_items_result(py, list, trace_id)? { + Ok(r) => r, + Err(violation) => return build_blocked_result(py, trace_id, violation, &self.cfg), + }; + + if was_modified { + let new_list = PyList::new(py, out_items)?; + let new_result_obj = new_list.into_any().unbind(); + let new_payload = clone_payload_with_attr(py, payload, "result", &new_result_obj)?; + let meta = PyDict::new(py); + meta.set_item("mcp_content_processed", true)?; + merge_metrics_into_meta( + py, + &meta, + trace_id, + total_chars, + true, + items_modified, + &self.cfg, + )?; + let kwargs: Vec<(&str, Py)> = vec![ + ("modified_payload", new_payload), + ("metadata", meta.into_any().unbind()), + ]; + return build_result_dyn(py, "ToolPostInvokeResult", kwargs); + } + let meta = PyDict::new(py); + meta.set_item("mcp_content_processed", true)?; + let kwargs: Vec<(&str, Py)> = vec![("metadata", meta.into_any().unbind())]; + build_result_dyn(py, "ToolPostInvokeResult", kwargs) + } + + fn handle_string_list( + &self, + py: Python<'_>, + payload: &Bound<'_, PyAny>, + list: &Bound<'_, PyList>, + trace_id: Option<&str>, + _name: &str, + ) -> PyResult> { + let mut modified = false; + let mut total_chars_truncated: usize = 0; + let mut items_modified: usize = 0; + let mut out: Vec = Vec::with_capacity(list.len()); + + for item in list.iter() { + let text: String = item.extract()?; + match handle_text(py, &text, &self.cfg)? { + TextResult::Violation(v) => { + return build_blocked_result(py, trace_id, v, &self.cfg); + } + TextResult::Modified(new_text) => { + total_chars_truncated += text.len(); + items_modified += 1; + out.push(new_text); + modified = true; + } + TextResult::BelowMin | TextResult::Unchanged => out.push(text), + } + } + + if modified { + let new_list = PyList::new(py, &out)?; + let new_result_obj = new_list.into_any().unbind(); + let new_payload = clone_payload_with_attr(py, payload, "result", &new_result_obj)?; + let meta = PyDict::new(py); + merge_metrics_into_meta( + py, + &meta, + trace_id, + total_chars_truncated, + true, + items_modified, + &self.cfg, + )?; + let kwargs: Vec<(&str, Py)> = vec![ + ("modified_payload", new_payload), + ("metadata", meta.into_any().unbind()), + ]; + return build_result_dyn(py, "ToolPostInvokeResult", kwargs); + } + let meta = PyDict::new(py); + let kwargs: Vec<(&str, Py)> = vec![("metadata", meta.into_any().unbind())]; + build_result_dyn(py, "ToolPostInvokeResult", kwargs) + } + + fn handle_mcp_content_dict( + &self, + py: Python<'_>, + payload: &Bound<'_, PyAny>, + result_dict: &Bound<'_, PyDict>, + content_list: &Bound<'_, PyList>, + trace_id: Option<&str>, + _name: &str, + ) -> PyResult> { + // PRIORITY: check structuredContent first + let struct_key = find_struct_key(result_dict)?; + + let mut struct_modified = false; + let mut new_result_dict_data: Option> = None; + + if let Some(sk) = &struct_key { + let struct_val = result_dict.get_item(sk.as_str())?.unwrap(); + match process_structured_data(py, &struct_val, &self.cfg, "", 0)? { + ProcessResult::Violation { + reason, + description, + code, + details, + } => { + let violation = build_violation(py, &reason, &description, &code, &details)?; + return build_blocked_result(py, trace_id, violation, &self.cfg); + } + ProcessResult::Ok { value, modified } => { + if modified { + struct_modified = true; + // Rebuild content from structured data + let new_text = generate_text_representation(value.bind(py), 0)?; + let content_item = PyDict::new(py); + content_item.set_item("type", "text")?; + content_item.set_item("text", &new_text)?; + let new_content = PyList::new(py, [content_item])?; + let value_ref = value.clone_ref(py); + let built = copy_dict_replace_keys( + py, + result_dict, + &[ + (sk.as_str(), value_ref), + ("content", new_content.into_any().unbind()), + ], + )?; + new_result_dict_data = Some(built); + } + } + } + } + + if struct_modified { + let nd = new_result_dict_data.unwrap(); + let new_payload = clone_payload_with_attr(py, payload, "result", &nd)?; + let meta = PyDict::new(py); + meta.set_item("mcp_result_processed", true)?; + meta.set_item("items_modified", true)?; + meta.set_item("structured_content_processed", true)?; + let kwargs: Vec<(&str, Py)> = vec![ + ("modified_payload", new_payload), + ("metadata", meta.into_any().unbind()), + ]; + return build_result_dyn(py, "ToolPostInvokeResult", kwargs); + } + + // Process content array + let (out_items, was_modified, total_chars_seen, items_modified_count) = + match self.process_mcp_items_result(py, content_list, trace_id)? { + Ok(r) => r, + Err(violation) => return build_blocked_result(py, trace_id, violation, &self.cfg), + }; + + let sc_processed = struct_key.is_some(); + + if was_modified { + let new_content_list = PyList::new(py, out_items)?; + let built = copy_dict_replace_keys( + py, + result_dict, + &[("content", new_content_list.into_any().unbind())], + )?; + let new_payload = clone_payload_with_attr(py, payload, "result", &built)?; + let meta = PyDict::new(py); + meta.set_item("mcp_result_processed", true)?; + meta.set_item("items_modified", true)?; + meta.set_item("structured_content_processed", sc_processed)?; + merge_metrics_into_meta( + py, + &meta, + trace_id, + total_chars_seen, + true, + items_modified_count, + &self.cfg, + )?; + let kwargs: Vec<(&str, Py)> = vec![ + ("modified_payload", new_payload), + ("metadata", meta.into_any().unbind()), + ]; + return build_result_dyn(py, "ToolPostInvokeResult", kwargs); + } + + let meta = PyDict::new(py); + meta.set_item("mcp_result_processed", true)?; + meta.set_item("items_modified", false)?; + meta.set_item("structured_content_processed", sc_processed)?; + let kwargs: Vec<(&str, Py)> = vec![("metadata", meta.into_any().unbind())]; + build_result_dyn(py, "ToolPostInvokeResult", kwargs) + } + + /// Process MCP content items, returning either (out_items, modified) or a violation. + #[allow(clippy::type_complexity)] + fn process_mcp_items_result( + &self, + py: Python<'_>, + list: &Bound<'_, PyList>, + _trace_id: Option<&str>, + ) -> PyResult>, bool, usize, usize), Py>> { + // Security: enforce max_structure_size on block strategy only. + // In truncate mode the list is allowed through so per-item text + // guarding below can still truncate individual oversized strings. + if list.len() > self.cfg.max_structure_size && self.cfg.strategy == Strategy::Block { + log::error!( + "Content list size {} exceeds maximum {} (MCP items)", + list.len(), + self.cfg.max_structure_size + ); + let violation = build_violation( + py, + "Structure size exceeds security limit", + &format!( + "Content list has {} items, exceeding limit of {}", + list.len(), + self.cfg.max_structure_size + ), + "STRUCTURE_SIZE_VIOLATION", + &[ + ("size".to_string(), serde_json::json!(list.len())), + ( + "max_size".to_string(), + serde_json::json!(self.cfg.max_structure_size), + ), + ], + )?; + return Ok(Err(violation)); + } + log_mcp_truncate_size_warning(list.len(), self.cfg.max_structure_size); + + let mut modified = false; + let mut total_chars_seen: usize = 0; + let mut items_modified_count: usize = 0; + let mut out: Vec> = Vec::with_capacity(list.len()); + + for item in list.iter() { + let Ok(item_dict) = item.cast::() else { + out.push(item.unbind()); + continue; + }; + + // text item + if item_dict + .get_item("type")? + .and_then(|v| v.extract::().ok()) + .as_deref() + == Some("text") + && let Some(text_val) = item_dict.get_item("text")? + && let Ok(text) = text_val.extract::() + { + match handle_text(py, &text, &self.cfg)? { + TextResult::Violation(v) => return Ok(Err(v)), + TextResult::Modified(new_text) => { + total_chars_seen += text.len(); + items_modified_count += 1; + let new_item = copy_dict_replace_keys( + py, + item_dict, + &[("text", new_text.into_pyobject(py)?.into_any().unbind())], + )?; + out.push(new_item); + modified = true; + continue; + } + TextResult::BelowMin | TextResult::Unchanged => {} + } + } + + // resource item + if item_dict + .get_item("type")? + .and_then(|v| v.extract::().ok()) + .as_deref() + == Some("resource") + && let Some(resource_val) = item_dict.get_item("resource")? + && let Ok(resource_dict) = resource_val.cast::() + && let Some(text_val) = resource_dict.get_item("text")? + && let Ok(text) = text_val.extract::() + { + match handle_text(py, &text, &self.cfg)? { + TextResult::Violation(v) => return Ok(Err(v)), + TextResult::Modified(new_text) => { + total_chars_seen += text.len(); + items_modified_count += 1; + let new_resource = copy_dict_replace_keys( + py, + resource_dict, + &[("text", new_text.into_pyobject(py)?.into_any().unbind())], + )?; + let new_item = + copy_dict_replace_keys(py, item_dict, &[("resource", new_resource)])?; + out.push(new_item); + modified = true; + continue; + } + TextResult::BelowMin | TextResult::Unchanged => {} + } + } + + out.push(item.unbind()); + } + + Ok(Ok((out, modified, total_chars_seen, items_modified_count))) + } +} + +// ─── Text handling ──────────────────────────────────────────────────────────── + +enum TextResult { + Unchanged, + /// Text was above-max and was truncated to `new_text`. + Modified(String), + /// Text was below min in truncate mode — pass through but flag as out-of-bounds. + BelowMin, + Violation(Py), +} + +fn handle_text(py: Python<'_>, text: &str, cfg: &OutputLengthGuardConfig) -> PyResult { + if is_numeric_string(text) { + return Ok(TextResult::Unchanged); + } + + let char_count = text.chars().count(); + let token_count = estimate_tokens(text, cfg.chars_per_token); + let (below_min, above_max) = evaluate_text_limits(char_count, token_count, cfg); + + if !below_min && !above_max { + return Ok(TextResult::Unchanged); + } + + if cfg.strategy == Strategy::Block { + let (reason, description, code, details) = + if above_max && cfg.limit_mode == LimitMode::Token { + ( + "Output estimated token count out of bounds".to_string(), + format!( + "Estimated token count {} exceeds max_tokens {}", + token_count, + cfg.max_tokens.unwrap_or(0) + ), + "OUTPUT_TOKEN_VIOLATION".to_string(), + vec![ + ("token_count".to_string(), serde_json::json!(token_count)), + ("max_tokens".to_string(), serde_json::json!(cfg.max_tokens)), + ( + "chars_per_token".to_string(), + serde_json::json!(cfg.chars_per_token), + ), + ( + "strategy".to_string(), + serde_json::json!(cfg.strategy.as_str()), + ), + ], + ) + } else if above_max { + ( + "Output length out of bounds".to_string(), + format!( + "Result length {} exceeds max_chars {}", + char_count, + cfg.max_chars.unwrap_or(0) + ), + "OUTPUT_LENGTH_VIOLATION".to_string(), + vec![ + ("length".to_string(), serde_json::json!(char_count)), + ("max_chars".to_string(), serde_json::json!(cfg.max_chars)), + ( + "strategy".to_string(), + serde_json::json!(cfg.strategy.as_str()), + ), + ], + ) + } else { + ( + "Output length below minimum".to_string(), + format!( + "Result length {} (tokens {}) below minimum", + char_count, token_count + ), + "OUTPUT_LENGTH_VIOLATION".to_string(), + vec![ + ("length".to_string(), serde_json::json!(char_count)), + ("min_chars".to_string(), serde_json::json!(cfg.min_chars)), + ("token_count".to_string(), serde_json::json!(token_count)), + ("min_tokens".to_string(), serde_json::json!(cfg.min_tokens)), + ( + "strategy".to_string(), + serde_json::json!(cfg.strategy.as_str()), + ), + ], + ) + }; + let violation = build_violation(py, &reason, &description, &code, &details)?; + return Ok(TextResult::Violation(violation)); + } + + // Truncate mode: only apply when above_max + if above_max { + let new_text = truncate(text, cfg); + if new_text != text { + return Ok(TextResult::Modified(new_text)); + } + } + + // Below min in truncate mode: pass through unchanged, signal out-of-bounds. + if below_min { + return Ok(TextResult::BelowMin); + } + + Ok(TextResult::Unchanged) +} + +// ─── Metrics helpers ────────────────────────────────────────────────────────── + +struct MetricsArgs<'a> { + chars_seen: usize, + truncated_count: usize, + blocked: bool, + mode: &'a str, + strategy: &'a str, + stage: &'a str, +} + +/// Build namespaced metrics dict for result.metadata. +/// Only emitted when trace_id is present. +fn build_output_metrics<'py>( + py: Python<'py>, + trace_id: Option<&str>, + args: MetricsArgs<'_>, +) -> PyResult>> { + if trace_id.is_none() { + return Ok(None); + } + let inner = PyDict::new(py); + inner.set_item("chars_seen", args.chars_seen)?; + inner.set_item("truncated_count", args.truncated_count)?; + inner.set_item("blocked", args.blocked)?; + inner.set_item("limit_mode", args.mode)?; + inner.set_item("strategy", args.strategy)?; + inner.set_item("stage", args.stage)?; + let outer = PyDict::new(py); + outer.set_item(PLUGIN_KEY, inner)?; + Ok(Some(outer)) +} + +/// Merge OTel metrics into an existing metadata dict. +/// +/// When a trace_id is present, builds the `output_length_guard` metrics dict and +/// inserts it as `meta["output_length_guard"]`. This avoids a second "metadata" +/// entry in the kwargs vec, which would silently overwrite the first one. +fn merge_metrics_into_meta( + py: Python<'_>, + meta: &Bound<'_, PyDict>, + trace_id: Option<&str>, + chars_seen: usize, + truncated: bool, + items_modified: usize, + cfg: &OutputLengthGuardConfig, +) -> PyResult<()> { + let Some(tid) = trace_id else { + return Ok(()); + }; + if let Some(outer) = build_output_metrics( + py, + Some(tid), + MetricsArgs { + chars_seen, + truncated_count: if truncated { items_modified } else { 0 }, + blocked: false, + mode: cfg.limit_mode.as_str(), + strategy: cfg.strategy.as_str(), + stage: "tool_post_invoke", + }, + )? { + // outer is { "output_length_guard": {...} } — merge its single entry into meta + if let Some(inner) = outer.get_item(PLUGIN_KEY)? { + meta.set_item(PLUGIN_KEY, inner)?; + } + } + Ok(()) +} + +// ─── Framework helpers ──────────────────────────────────────────────────────── + +#[mutants::skip] // log-only branch: > vs == / < / >= are all equivalent — no observable effect +fn log_mcp_truncate_size_warning(len: usize, max: usize) { + if len > max { + log::error!( + "Content list size {} exceeds maximum {} (MCP items), guarding individual items", + len, + max + ); + } +} + +fn build_violation( + py: Python<'_>, + reason: &str, + description: &str, + code: &str, + details: &[(String, serde_json::Value)], +) -> PyResult> { + let details_dict = PyDict::new(py); + for (k, v) in details { + let py_val: Py = json_to_py(py, v)?; + details_dict.set_item(k, py_val.bind(py))?; + } + // mcp_error_code=-32000 (Application error) and http_status_code=422 are + // required by the gateway contract; without them the exception handler falls + // back to JSON-RPC -32603 and an incorrect HTTP status. + build_framework_object_dyn( + py, + "PluginViolation", + vec![ + ("reason", reason.into_pyobject(py)?.into_any().unbind()), + ( + "description", + description.into_pyobject(py)?.into_any().unbind(), + ), + ("code", code.into_pyobject(py)?.into_any().unbind()), + ("details", details_dict.into_any().unbind()), + ( + "mcp_error_code", + (-32000_i64).into_pyobject(py)?.into_any().unbind(), + ), + ( + "http_status_code", + 422_i64.into_pyobject(py)?.into_any().unbind(), + ), + ], + ) +} + +fn build_blocked_result( + py: Python<'_>, + trace_id: Option<&str>, + violation: Py, + cfg: &OutputLengthGuardConfig, +) -> PyResult> { + let mut kwargs: Vec<(&str, Py)> = vec![ + ( + "continue_processing", + false.into_pyobject(py)?.to_owned().into_any().unbind(), + ), + ("violation", violation), + ]; + if let Some(tid) = trace_id + && let Ok(Some(md)) = build_output_metrics( + py, + Some(tid), + MetricsArgs { + chars_seen: 0, + truncated_count: 0, + blocked: true, + mode: cfg.limit_mode.as_str(), + strategy: cfg.strategy.as_str(), + stage: "tool_post_invoke", + }, + ) + { + kwargs.push(("metadata", md.into_any().unbind())); + } + build_result_dyn(py, "ToolPostInvokeResult", kwargs) +} + +fn build_result_dyn( + py: Python<'_>, + class_name: &str, + kwargs: Vec<(&str, Py)>, +) -> PyResult> { + build_framework_object_dyn(py, class_name, kwargs) +} + +fn default_result(py: Python<'_>, class_name: &str) -> PyResult> { + bridge_default_result(py, class_name) +} + +fn default_result_with_meta( + py: Python<'_>, + class_name: &str, + meta: Bound<'_, PyDict>, +) -> PyResult> { + build_framework_object_dyn(py, class_name, vec![("metadata", meta.into_any().unbind())]) +} + +// ─── Payload mutation helpers ───────────────────────────────────────────────── + +fn clone_payload_with_attr( + py: Python<'_>, + payload: &Bound<'_, PyAny>, + attr: &str, + new_value: &Py, +) -> PyResult> { + let cloned = if payload.hasattr("model_copy")? { + let kwargs = PyDict::new(py); + let update = PyDict::new(py); + update.set_item(attr, new_value.bind(py))?; + kwargs.set_item("update", update)?; + payload.call_method("model_copy", (), Some(&kwargs))? + } else { + let copy = PyModule::import(py, "copy")?; + let cloned = copy.getattr("copy")?.call1((payload,))?; + cloned.setattr(attr, new_value.bind(py))?; + cloned + }; + Ok(cloned.unbind()) +} + +/// Clone a dict with one key replaced. +fn clone_dict_with_key( + py: Python<'_>, + dict: &Bound<'_, PyDict>, + key: &str, + new_val: &str, +) -> PyResult> { + let out = PyDict::new(py); + for (k, v) in dict.iter() { + out.set_item(&k, &v)?; + } + out.set_item(key, new_val)?; + Ok(out.into_any().unbind()) +} + +/// Copy a dict and replace the given key/value pairs. +fn copy_dict_replace_keys( + py: Python<'_>, + dict: &Bound<'_, PyDict>, + replacements: &[(&str, Py)], +) -> PyResult> { + let out = PyDict::new(py); + for (k, v) in dict.iter() { + out.set_item(&k, &v)?; + } + for (key, val) in replacements { + out.set_item(*key, val.bind(py))?; + } + Ok(out.into_any().unbind()) +} + +fn find_struct_key(result_dict: &Bound<'_, PyDict>) -> PyResult> { + for key in ["structuredContent", "structured_content"] { + if let Some(val) = result_dict.get_item(key)? + && !val.is_none() + { + return Ok(Some(key.to_string())); + } + } + Ok(None) +} + +fn json_to_py(py: Python<'_>, v: &serde_json::Value) -> PyResult> { + match v { + serde_json::Value::Null => Ok(py.None()), + serde_json::Value::Bool(b) => Ok(b.into_pyobject(py)?.to_owned().into_any().unbind()), + serde_json::Value::Number(n) => { + if let Some(i) = n.as_i64() { + Ok(i.into_pyobject(py)?.into_any().unbind()) + } else if let Some(f) = n.as_f64() { + Ok(f.into_pyobject(py)?.into_any().unbind()) + } else { + Ok(n.to_string().into_pyobject(py)?.into_any().unbind()) + } + } + serde_json::Value::String(s) => Ok(s.into_pyobject(py)?.into_any().unbind()), + serde_json::Value::Array(arr) => { + let list = PyList::empty(py); + for item in arr { + list.append(json_to_py(py, item)?.bind(py))?; + } + Ok(list.into_any().unbind()) + } + serde_json::Value::Object(map) => { + let d = PyDict::new(py); + for (k, v) in map { + d.set_item(k, json_to_py(py, v)?.bind(py))?; + } + Ok(d.into_any().unbind()) + } + } +} + +fn new_text_str(obj: &Py, py: Python<'_>) -> String { + obj.bind(py).extract::().unwrap_or_default() +} + +fn build_text_meta_dict<'py>( + py: Python<'py>, + original: &str, + new_text: &str, + within_bounds: bool, +) -> PyResult> { + let meta = PyDict::new(py); + meta.set_item("original_length", original.len())?; + meta.set_item("within_bounds", within_bounds)?; + if !within_bounds { + meta.set_item("truncated", new_text != original)?; + meta.set_item("new_length", new_text.len())?; + } + Ok(meta) +} + +/// Extract trace_id from extensions.request.trace_id +fn read_trace_id(extensions: Option<&Bound<'_, PyAny>>) -> Option { + let ext = extensions?; + let request = ext.getattr("request").ok()?; + if request.is_none() { + return None; + } + let trace = request.getattr("trace_id").ok()?; + if trace.is_none() { + return None; + } + let s: String = trace.extract().ok()?; + if s.is_empty() { None } else { Some(s) } +} + +// ─── Tests ──────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use pyo3::types::{PyDict, PyList, PyModule}; + + fn install_framework_module(py: Python<'_>) -> PyResult<()> { + let framework = PyModule::from_code( + py, + pyo3::ffi::c_str!( + r#" +class ToolPostInvokeResult: + def __init__(self, modified_payload=None, continue_processing=True, violation=None, metadata=None): + self.modified_payload = modified_payload + self.continue_processing = continue_processing + self.violation = violation + self.metadata = metadata + +class PluginViolation: + def __init__(self, reason, code, description=None, details=None, mcp_error_code=None, http_status_code=None): + self.reason = reason + self.code = code + self.description = description + self.details = details + self.mcp_error_code = mcp_error_code + self.http_status_code = http_status_code +"# + ), + pyo3::ffi::c_str!("framework.py"), + pyo3::ffi::c_str!("cpex.framework"), + )?; + let cpex = PyModule::from_code( + py, + pyo3::ffi::c_str!(""), + pyo3::ffi::c_str!("cpex.py"), + pyo3::ffi::c_str!("cpex"), + )?; + cpex.setattr("framework", &framework)?; + let modules = PyModule::import(py, "sys")? + .getattr("modules")? + .cast_into::()?; + modules.set_item("cpex", cpex)?; + modules.set_item("cpex.framework", framework)?; + Ok(()) + } + + fn make_core( + max_chars: Option, + strategy: &str, + ) -> PyResult { + pyo3::Python::attach(|py| { + let d = PyDict::new(py); + match max_chars { + Some(n) => d.set_item("max_chars", n)?, + None => d.set_item("max_chars", py.None())?, + } + d.set_item("strategy", strategy)?; + d.set_item("limit_mode", "character")?; + OutputLengthGuardPluginCore::new(d.as_any()) + }) + } + + fn make_payload<'py>( + py: Python<'py>, + name: &str, + result: Bound<'py, PyAny>, + ) -> PyResult> { + let module = PyModule::from_code( + py, + pyo3::ffi::c_str!( + r#" +class Payload: + def __init__(self, name, result): + self.name = name + self.result = result +"# + ), + pyo3::ffi::c_str!("payload.py"), + pyo3::ffi::c_str!("payload"), + )?; + module.getattr("Payload")?.call1((name, result)) + } + + #[test] + fn truncates_long_plain_string() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(10), "truncate").unwrap(); + let text = "A".repeat(100).into_pyobject(py).unwrap().into_any(); + let payload = make_payload(py, "tool1", text).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let result = result.bind(py); + let modified = result.getattr("modified_payload").unwrap(); + assert!(!modified.is_none()); + let new_result: String = modified.getattr("result").unwrap().extract().unwrap(); + assert!(new_result.chars().count() <= 10); + }); + } + + #[test] + fn blocks_long_plain_string_in_block_mode() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(10), "block").unwrap(); + let text = "A".repeat(100).into_pyobject(py).unwrap().into_any(); + let payload = make_payload(py, "tool1", text).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let result = result.bind(py); + let cp: bool = result + .getattr("continue_processing") + .unwrap() + .extract() + .unwrap(); + assert!(!cp); + assert!(!result.getattr("violation").unwrap().is_none()); + }); + } + + #[test] + fn short_plain_string_passes_through_unchanged() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(1000), "truncate").unwrap(); + let text = "hello".into_pyobject(py).unwrap().into_any(); + let payload = make_payload(py, "tool1", text).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let result = result.bind(py); + assert!(result.getattr("modified_payload").unwrap().is_none()); + }); + } + + #[test] + fn dict_with_text_field_is_truncated() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(5), "truncate").unwrap(); + let d = PyDict::new(py); + d.set_item("text", "hello world foo").unwrap(); + let payload = make_payload(py, "t", d.as_any().clone()).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let result = result.bind(py); + let modified = result.getattr("modified_payload").unwrap(); + assert!(!modified.is_none()); + }); + } + + #[test] + fn dict_without_text_field_passes_through() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(5), "truncate").unwrap(); + let d = PyDict::new(py); + d.set_item("other", "value").unwrap(); + let payload = make_payload(py, "t", d.as_any().clone()).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let result = result.bind(py); + assert!(result.getattr("modified_payload").unwrap().is_none()); + }); + } + + #[test] + fn mcp_list_text_item_is_truncated() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(5), "truncate").unwrap(); + let item = PyDict::new(py); + item.set_item("type", "text").unwrap(); + item.set_item("text", "hello world foo").unwrap(); + let list = PyList::new(py, [item]).unwrap(); + let payload = make_payload(py, "t", list.as_any().clone()).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let result = result.bind(py); + let modified = result.getattr("modified_payload").unwrap(); + assert!(!modified.is_none()); + }); + } + + #[test] + fn mcp_content_dict_with_text_item_is_truncated() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(5), "truncate").unwrap(); + let item = PyDict::new(py); + item.set_item("type", "text").unwrap(); + item.set_item("text", "hello world foo").unwrap(); + let content_list = PyList::new(py, [item]).unwrap(); + let result_dict = PyDict::new(py); + result_dict.set_item("content", content_list).unwrap(); + let payload = make_payload(py, "t", result_dict.as_any().clone()).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let result = result.bind(py); + let modified = result.getattr("modified_payload").unwrap(); + assert!(!modified.is_none()); + }); + } + + #[test] + fn string_list_is_truncated() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(5), "truncate").unwrap(); + let list = PyList::new(py, ["hello world", "short"]).unwrap(); + let payload = make_payload(py, "t", list.as_any().clone()).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let result = result.bind(py); + let modified = result.getattr("modified_payload").unwrap(); + assert!(!modified.is_none()); + }); + } + + #[test] + fn metrics_emitted_only_when_trace_id_present() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let md = build_output_metrics( + py, + Some("t1"), + MetricsArgs { + chars_seen: 100, + truncated_count: 1, + blocked: false, + mode: "character", + strategy: "truncate", + stage: "tool_post_invoke", + }, + ) + .unwrap(); + assert!(md.is_some()); + let outer_dict = md.unwrap(); + let inner = outer_dict.get_item(PLUGIN_KEY).unwrap().unwrap(); + // Verify "chars_seen" key is present and not None + let inner_dict = inner.cast::().unwrap(); + assert!(inner_dict.contains("chars_seen").unwrap()); + // No trace => None + let md2 = build_output_metrics( + py, + None, + MetricsArgs { + chars_seen: 100, + truncated_count: 1, + blocked: false, + mode: "character", + strategy: "truncate", + stage: "tool_post_invoke", + }, + ) + .unwrap(); + assert!(md2.is_none()); + }); + } + + #[test] + fn read_trace_id_returns_value_when_present() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let module = PyModule::from_code( + py, + pyo3::ffi::c_str!( + "class Req:\n def __init__(self, t):\n self.trace_id = t\n\ + class Ext:\n def __init__(self, t):\n self.request = Req(t)\n" + ), + pyo3::ffi::c_str!("ext.py"), + pyo3::ffi::c_str!("ext"), + ) + .unwrap(); + let with_id = module.getattr("Ext").unwrap().call1(("abc123",)).unwrap(); + let without = module.getattr("Ext").unwrap().call1((py.None(),)).unwrap(); + assert_eq!(read_trace_id(Some(&with_id)), Some("abc123".to_string())); + assert_eq!(read_trace_id(Some(&without)), None); + assert_eq!(read_trace_id(None), None); + }); + } + + #[test] + fn no_raw_content_in_metrics() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let md = build_output_metrics( + py, + Some("t1"), + MetricsArgs { + chars_seen: 42, + truncated_count: 1, + blocked: false, + mode: "character", + strategy: "truncate", + stage: "tool_post_invoke", + }, + ) + .unwrap() + .unwrap(); + let inner = md.get_item(PLUGIN_KEY).unwrap().unwrap(); + let dumped = format!("{:?}", inner.str().unwrap()); + // No actual text content should be in the metrics + assert!(!dumped.contains("hello world")); + }); + } + + #[test] + fn numeric_string_passes_through_without_modification() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(2), "block").unwrap(); + let text = "42".into_pyobject(py).unwrap().into_any(); + let payload = make_payload(py, "t", text).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let result = result.bind(py); + // Numeric strings should pass through unchanged (no violation) + let cp: bool = result + .getattr("continue_processing") + .unwrap() + .extract() + .unwrap(); + assert!(cp); + }); + } + + #[test] + fn token_mode_truncates_by_estimated_tokens() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let d = PyDict::new(py); + d.set_item("max_tokens", 2).unwrap(); // 2 tokens * 4 chars = 8 chars + d.set_item("limit_mode", "token").unwrap(); + d.set_item("strategy", "truncate").unwrap(); + d.set_item("max_chars", py.None()).unwrap(); + let core = OutputLengthGuardPluginCore::new(d.as_any()).unwrap(); + let text = "abcdefghijklmnop".into_pyobject(py).unwrap().into_any(); // 16 chars = 4 tokens + let payload = make_payload(py, "t", text).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let result = result.bind(py); + let modified = result.getattr("modified_payload").unwrap(); + assert!(!modified.is_none()); + }); + } + + // ── handle_string_list counter accuracy ────────────────────────────── + // Kill: replace += with *= on total_chars_truncated / items_modified (lines 218-219) + // We verify metrics are accurate by asserting the modified_payload is produced + // (metrics path depends on correct counter values being > 0) + #[test] + fn string_list_two_items_truncated_both_modified() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(3), "truncate").unwrap(); + // Both strings exceed max_chars=3 + let list = PyList::new(py, ["hello", "world"]).unwrap(); + let payload = make_payload(py, "t", list.as_any().clone()).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let result = result.bind(py); + let modified = result.getattr("modified_payload").unwrap(); + assert!(!modified.is_none(), "both strings should be truncated"); + // Verify each item in the result list is within max_chars + let result_obj = modified.getattr("result").unwrap(); + let result_list = result_obj.cast::().unwrap(); + for item in result_list.iter() { + let s: String = item.extract().unwrap(); + assert!(s.chars().count() <= 3, "item '{}' exceeds max_chars", s); + } + }); + } + + // ── process_mcp_items_result structure size guard boundary ──────────── + // Kill: replace > with == / < / >= in size check (line 369) + #[test] + fn mcp_content_dict_at_exactly_max_structure_size_is_not_blocked() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let d = PyDict::new(py); + d.set_item("max_chars", py.None()).unwrap(); // no char limit + d.set_item("max_structure_size", 3usize).unwrap(); + d.set_item("strategy", "block").unwrap(); + d.set_item("limit_mode", "character").unwrap(); + let core = OutputLengthGuardPluginCore::new(d.as_any()).unwrap(); + // Exactly 3 items == max_structure_size: must NOT block + let item_a = PyDict::new(py); + item_a.set_item("type", "text").unwrap(); + item_a.set_item("text", "a").unwrap(); + let item_b = PyDict::new(py); + item_b.set_item("type", "text").unwrap(); + item_b.set_item("text", "b").unwrap(); + let item_c = PyDict::new(py); + item_c.set_item("type", "text").unwrap(); + item_c.set_item("text", "c").unwrap(); + let content = PyList::new(py, [item_a, item_b, item_c]).unwrap(); + let result_dict = PyDict::new(py); + result_dict.set_item("content", content).unwrap(); + let payload = make_payload(py, "t", result_dict.as_any().clone()).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let cp: bool = result + .bind(py) + .getattr("continue_processing") + .unwrap() + .extract() + .unwrap(); + assert!(cp, "exactly max_structure_size items must not block"); + }); + } + + // Kill: replace == with != in resource item type check (line 433) + #[test] + fn mcp_content_dict_resource_item_text_is_truncated() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(5), "truncate").unwrap(); + let resource = PyDict::new(py); + resource.set_item("text", "hello world foo bar").unwrap(); + let item = PyDict::new(py); + item.set_item("type", "resource").unwrap(); + item.set_item("resource", resource).unwrap(); + let content = PyList::new(py, [item]).unwrap(); + let result_dict = PyDict::new(py); + result_dict.set_item("content", content).unwrap(); + let payload = make_payload(py, "t", result_dict.as_any().clone()).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let result = result.bind(py); + let modified = result.getattr("modified_payload").unwrap(); + assert!(!modified.is_none(), "resource text should be truncated"); + }); + } + + // Kill: replace += with -= / *= on resource item counters (lines 442-443) + #[test] + fn mcp_content_dict_resource_item_counter_is_nonzero_after_truncation() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(3), "truncate").unwrap(); + let resource = PyDict::new(py); + resource.set_item("text", "toolongtext").unwrap(); + let item = PyDict::new(py); + item.set_item("type", "resource").unwrap(); + item.set_item("resource", resource).unwrap(); + let content = PyList::new(py, [item]).unwrap(); + let result_dict = PyDict::new(py); + result_dict.set_item("content", content).unwrap(); + let payload = make_payload(py, "t", result_dict.as_any().clone()).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + // modified_payload must be present (counter > 0 required to trigger the modified branch) + let modified = result.bind(py).getattr("modified_payload").unwrap(); + assert!( + !modified.is_none(), + "resource item truncation must produce modified_payload" + ); + }); + } + + // ── handle_text: !below_min && !above_max short-circuit ────────────── + // Kill: delete ! in `if !below_min && !above_max` (line 483) + #[test] + fn handle_text_within_bounds_does_not_modify_payload() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + // max_chars=100, no min — "hello" is well within bounds + let core = make_core(Some(100), "block").unwrap(); + let text = "hello".into_pyobject(py).unwrap().into_any(); + let payload = make_payload(py, "t", text).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let result = result.bind(py); + // continue_processing must be true and no modification + let cp: bool = result + .getattr("continue_processing") + .unwrap() + .extract() + .unwrap(); + assert!(cp); + assert!(result.getattr("modified_payload").unwrap().is_none()); + }); + } + + // Kill: replace && with || in `above_max && cfg.limit_mode == LimitMode::Token` (line 489) + #[test] + fn handle_text_char_mode_violation_code_is_output_length_not_token() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(5), "block").unwrap(); // character mode, block + let text = "this is a long string" + .into_pyobject(py) + .unwrap() + .into_any(); + let payload = make_payload(py, "t", text).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let result = result.bind(py); + let violation = result.getattr("violation").unwrap(); + assert!(!violation.is_none()); + let code: String = violation.getattr("code").unwrap().extract().unwrap(); + // character mode must emit OUTPUT_LENGTH_VIOLATION, not OUTPUT_TOKEN_VIOLATION + assert_eq!(code, "OUTPUT_LENGTH_VIOLATION"); + }); + } + + // Kill: replace == with != in the token check (line 489) + #[test] + fn handle_text_token_mode_violation_code_is_output_token_violation() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let d = PyDict::new(py); + d.set_item("max_tokens", 1usize).unwrap(); // 1 token * 4 = 4 chars + d.set_item("limit_mode", "token").unwrap(); + d.set_item("strategy", "block").unwrap(); + d.set_item("max_chars", py.None()).unwrap(); + let core = OutputLengthGuardPluginCore::new(d.as_any()).unwrap(); + let text = "abcdefghij".into_pyobject(py).unwrap().into_any(); // 10 chars = 2+ tokens + let payload = make_payload(py, "t", text).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let result = result.bind(py); + let violation = result.getattr("violation").unwrap(); + assert!(!violation.is_none()); + let code: String = violation.getattr("code").unwrap().extract().unwrap(); + assert_eq!(code, "OUTPUT_TOKEN_VIOLATION"); + }); + } + + // ── find_struct_key: !val.is_none() guard (line 762) ───────────────── + // Kill: delete ! in `!val.is_none()` + // A None-valued structuredContent key must NOT be treated as present + #[test] + fn mcp_content_dict_null_structured_content_treated_as_absent() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(5), "truncate").unwrap(); + let item = PyDict::new(py); + item.set_item("type", "text").unwrap(); + item.set_item("text", "hello world foo").unwrap(); + let content = PyList::new(py, [item]).unwrap(); + let result_dict = PyDict::new(py); + result_dict.set_item("content", content).unwrap(); + result_dict + .set_item("structuredContent", py.None()) + .unwrap(); + let payload = make_payload(py, "t", result_dict.as_any().clone()).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + // Even with structuredContent=None, content text must be truncated + let modified = result.bind(py).getattr("modified_payload").unwrap(); + assert!( + !modified.is_none(), + "content text must be truncated even when structuredContent is null" + ); + }); + } + + // ── build_text_meta: !within_bounds branch (line 814) ──────────────── + // Kill: delete ! in `if !within_bounds` and replace != with == (line 815) + #[test] + fn truncated_string_metadata_contains_truncated_true_and_new_length() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(5), "truncate").unwrap(); + let text = "hello world".into_pyobject(py).unwrap().into_any(); + let payload = make_payload(py, "t", text).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let result = result.bind(py); + let meta = result.getattr("metadata").unwrap(); + // metadata must include within_bounds=false, truncated=true, new_length + let within_bounds: bool = meta.get_item("within_bounds").unwrap().extract().unwrap(); + assert!(!within_bounds); + let truncated: bool = meta.get_item("truncated").unwrap().extract().unwrap(); + assert!(truncated, "truncated must be true when text was shortened"); + // new_length is byte length of the truncated result; the original had 11 chars + // so it must be strictly less than the original (11 bytes for ASCII) + let new_length: usize = meta.get_item("new_length").unwrap().extract().unwrap(); + assert!( + new_length < 11, + "new_length must be less than original length" + ); + }); + } + + #[test] + fn within_bounds_string_metadata_does_not_contain_new_length() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(100), "truncate").unwrap(); + let text = "hi".into_pyobject(py).unwrap().into_any(); + let payload = make_payload(py, "t", text).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let result = result.bind(py); + let meta = result.getattr("metadata").unwrap(); + let within_bounds: bool = meta.get_item("within_bounds").unwrap().extract().unwrap(); + assert!(within_bounds); + // within_bounds=true: key "truncated" must NOT be present in the dict. + let meta_dict = meta.cast::().unwrap(); + let has_truncated = meta_dict.get_item("truncated").unwrap().is_some(); + assert!( + !has_truncated, + "within_bounds path must not set 'truncated'" + ); + }); + } + + // ── new_text_str (plugin.rs:802): replace with String::new() or "xyzzy" ── + // The function is called to produce the `new_text` string used in build_text_meta. + // build_text_meta sets new_length = new_text.len(). If the mutant returns "", + // new_length == 0; if it returns "xyzzy", new_length == 5 (always). + // We kill both by asserting new_length > 0 AND new_length != 5. + #[test] + fn truncated_plain_string_new_length_is_positive_and_not_xyzzy() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(8), "truncate").unwrap(); + // "hello world" = 11 chars > 8 → truncated. Truncated result has 7 chars + // (max_chars - ell_chars = 8 - 1 = 7) + "…" = 8 chars ≈ >5 bytes. + let text = "hello world".into_pyobject(py).unwrap().into_any(); + let payload = make_payload(py, "t", text).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let result = result.bind(py); + let meta = result.getattr("metadata").unwrap(); + let new_length: usize = meta.get_item("new_length").unwrap().extract().unwrap(); + assert!(new_length > 0, "new_length must be > 0, got {}", new_length); + // Kill "xyzzy" mutant: the actual truncated "hello w…" is 10 bytes, not 5. + assert_ne!(new_length, 5, "new_length must not be 5 (xyzzy length)"); + }); + } + + // ── handle_string_list += counters (lines 218-219): += vs *= ───────────── + // With *=, total_chars_truncated and items_modified stay at 0 forever. + // push_metrics_kwargs is a no-op without trace_id, so we need to use a trace_id + // and verify the emitted metadata reflects non-zero chars_seen / truncated_count. + // We use a helper to inject an extensions object carrying a trace_id. + fn make_extensions<'py>(py: Python<'py>, trace_id: &str) -> PyResult> { + let module = PyModule::from_code( + py, + pyo3::ffi::c_str!( + "class Req:\n def __init__(self, t):\n self.trace_id = t\n\ + class Ext:\n def __init__(self, t):\n self.request = Req(t)\n" + ), + pyo3::ffi::c_str!("ext2.py"), + pyo3::ffi::c_str!("ext2"), + )?; + module.getattr("Ext")?.call1((trace_id,)) + } + + #[test] + fn string_list_with_trace_id_metrics_have_nonzero_chars_seen() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(3), "truncate").unwrap(); + // "hello" = 5 chars > 3 → truncated. total_chars_seen += 5 → must be 5, not 0. + let list = PyList::new(py, ["hello"]).unwrap(); + let payload = make_payload(py, "t", list.as_any().clone()).unwrap(); + let ctx = PyDict::new(py); + let ext = make_extensions(py, "trace-abc").unwrap(); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), Some(&ext)) + .unwrap(); + let result = result.bind(py); + // metadata should be the metrics dict {output_length_guard: {...}} + let meta = result.getattr("metadata").unwrap(); + // The metrics dict is keyed by PLUGIN_KEY = "output_length_guard" + let inner = meta.get_item("output_length_guard").unwrap(); + assert!( + !inner.is_none(), + "output_length_guard metrics must be present" + ); + let chars_seen: usize = inner.get_item("chars_seen").unwrap().extract().unwrap(); + assert!(chars_seen > 0, "chars_seen must be > 0, got {}", chars_seen); + let truncated_count: usize = inner + .get_item("truncated_count") + .unwrap() + .extract() + .unwrap(); + assert!( + truncated_count > 0, + "truncated_count must be > 0, got {}", + truncated_count + ); + }); + } + + // ── process_mcp_items_result += counters (lines 413-414): text items ───── + #[test] + fn mcp_content_dict_with_trace_id_metrics_have_nonzero_chars_seen() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(3), "truncate").unwrap(); + let item = PyDict::new(py); + item.set_item("type", "text").unwrap(); + item.set_item("text", "hello world").unwrap(); // 11 chars > 3 + let content = PyList::new(py, [item]).unwrap(); + let result_dict = PyDict::new(py); + result_dict.set_item("content", content).unwrap(); + let payload = make_payload(py, "t", result_dict.as_any().clone()).unwrap(); + let ctx = PyDict::new(py); + let ext = make_extensions(py, "trace-def").unwrap(); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), Some(&ext)) + .unwrap(); + let result = result.bind(py); + let meta = result.getattr("metadata").unwrap(); + let inner = meta.get_item("output_length_guard").unwrap(); + assert!( + !inner.is_none(), + "output_length_guard metrics must be present" + ); + let chars_seen: usize = inner.get_item("chars_seen").unwrap().extract().unwrap(); + assert!(chars_seen > 0, "chars_seen must be > 0, got {}", chars_seen); + }); + } + + // ── process_mcp_items_result += counters (lines 442-443): resource items ── + #[test] + fn mcp_resource_item_with_trace_id_metrics_have_nonzero_chars_seen() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(3), "truncate").unwrap(); + let resource = PyDict::new(py); + resource.set_item("text", "toolongtext").unwrap(); // 11 chars > 3 + let item = PyDict::new(py); + item.set_item("type", "resource").unwrap(); + item.set_item("resource", resource).unwrap(); + let content = PyList::new(py, [item]).unwrap(); + let result_dict = PyDict::new(py); + result_dict.set_item("content", content).unwrap(); + let payload = make_payload(py, "t", result_dict.as_any().clone()).unwrap(); + let ctx = PyDict::new(py); + let ext = make_extensions(py, "trace-ghi").unwrap(); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), Some(&ext)) + .unwrap(); + let result = result.bind(py); + let meta = result.getattr("metadata").unwrap(); + let inner = meta.get_item("output_length_guard").unwrap(); + assert!( + !inner.is_none(), + "output_length_guard metrics must be present for resource item" + ); + let chars_seen: usize = inner.get_item("chars_seen").unwrap().extract().unwrap(); + assert!( + chars_seen > 0, + "chars_seen must be > 0 for resource item, got {}", + chars_seen + ); + }); + } + + // ── process_mcp_items_result > vs < on structure size (line 369) ───────── + // Kill: replace > with <: a list SMALLER than max_structure_size must NOT block. + #[test] + fn mcp_content_dict_under_max_structure_size_is_not_blocked() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let d = PyDict::new(py); + d.set_item("max_chars", py.None()).unwrap(); + d.set_item("max_structure_size", 10usize).unwrap(); + d.set_item("strategy", "block").unwrap(); + d.set_item("limit_mode", "character").unwrap(); + let core = OutputLengthGuardPluginCore::new(d.as_any()).unwrap(); + // Only 2 items — well under the limit of 10 + let item_a = PyDict::new(py); + item_a.set_item("type", "text").unwrap(); + item_a.set_item("text", "a").unwrap(); + let item_b = PyDict::new(py); + item_b.set_item("type", "text").unwrap(); + item_b.set_item("text", "b").unwrap(); + let content = PyList::new(py, [item_a, item_b]).unwrap(); + let result_dict = PyDict::new(py); + result_dict.set_item("content", content).unwrap(); + let payload = make_payload(py, "t", result_dict.as_any().clone()).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let cp: bool = result + .bind(py) + .getattr("continue_processing") + .unwrap() + .extract() + .unwrap(); + assert!( + cp, + "list with 2 items < max_structure_size=10 must not block" + ); + }); + } + + // ── process_mcp_items_result line 369: == with != on Strategy::Block ───── + // Kill: replace == with != → truncate strategy would trigger the block. + // Test: strategy=truncate (not Block), oversized list → must NOT block. + #[test] + fn mcp_content_dict_oversized_list_in_truncate_mode_is_not_blocked() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let d = PyDict::new(py); + d.set_item("max_chars", py.None()).unwrap(); + d.set_item("max_structure_size", 2usize).unwrap(); + // Truncate strategy — oversized list must NOT be blocked + d.set_item("strategy", "truncate").unwrap(); + d.set_item("limit_mode", "character").unwrap(); + let core = OutputLengthGuardPluginCore::new(d.as_any()).unwrap(); + // 5 items > max_structure_size=2, but strategy=truncate → must NOT block + let item_a = PyDict::new(py); + item_a.set_item("type", "text").unwrap(); + item_a.set_item("text", "a").unwrap(); + let item_b = PyDict::new(py); + item_b.set_item("type", "text").unwrap(); + item_b.set_item("text", "b").unwrap(); + let item_c = PyDict::new(py); + item_c.set_item("type", "text").unwrap(); + item_c.set_item("text", "c").unwrap(); + let content = PyList::new(py, [item_a, item_b, item_c]).unwrap(); + let result_dict = PyDict::new(py); + result_dict.set_item("content", content).unwrap(); + let payload = make_payload(py, "t", result_dict.as_any().clone()).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let cp: bool = result + .bind(py) + .getattr("continue_processing") + .unwrap() + .extract() + .unwrap(); + assert!( + cp, + "truncate strategy must not block oversized list; != mutant would block" + ); + }); + } + + // ── process_mcp_items_result lines 414, 443: items_modified_count += vs *= ─ + // truncated_count = items_modified_count. With *= mutant, items_modified_count stays 0. + #[test] + fn mcp_content_dict_text_item_trace_id_truncated_count_is_nonzero() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(3), "truncate").unwrap(); + let item = PyDict::new(py); + item.set_item("type", "text").unwrap(); + item.set_item("text", "hello world").unwrap(); + let content = PyList::new(py, [item]).unwrap(); + let result_dict = PyDict::new(py); + result_dict.set_item("content", content).unwrap(); + let payload = make_payload(py, "t", result_dict.as_any().clone()).unwrap(); + let ctx = PyDict::new(py); + let ext = make_extensions(py, "trace-xyz").unwrap(); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), Some(&ext)) + .unwrap(); + let result = result.bind(py); + let meta = result.getattr("metadata").unwrap(); + let inner = meta.get_item("output_length_guard").unwrap(); + assert!(!inner.is_none()); + let truncated_count: usize = inner + .get_item("truncated_count") + .unwrap() + .extract() + .unwrap(); + assert!( + truncated_count > 0, + "truncated_count must be > 0 (items_modified_count += 1), got {}", + truncated_count + ); + }); + } + + #[test] + fn mcp_resource_item_trace_id_truncated_count_is_nonzero() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(3), "truncate").unwrap(); + let resource = PyDict::new(py); + resource.set_item("text", "toolongtext").unwrap(); + let item = PyDict::new(py); + item.set_item("type", "resource").unwrap(); + item.set_item("resource", resource).unwrap(); + let content = PyList::new(py, [item]).unwrap(); + let result_dict = PyDict::new(py); + result_dict.set_item("content", content).unwrap(); + let payload = make_payload(py, "t", result_dict.as_any().clone()).unwrap(); + let ctx = PyDict::new(py); + let ext = make_extensions(py, "trace-res").unwrap(); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), Some(&ext)) + .unwrap(); + let result = result.bind(py); + let meta = result.getattr("metadata").unwrap(); + let inner = meta.get_item("output_length_guard").unwrap(); + assert!(!inner.is_none()); + let truncated_count: usize = inner + .get_item("truncated_count") + .unwrap() + .extract() + .unwrap(); + assert!( + truncated_count > 0, + "truncated_count must be > 0 for resource item, got {}", + truncated_count + ); + }); + } + + // ── find_struct_key line 762: delete ! in !val.is_none() ───────────────── + // With ! deleted: val.is_none() → true means "present" → None-valued key treated as found. + // The existing test checks truncation still happens with None structuredContent. + // But with the mutant, None-valued structuredContent IS found → process_structured_data + // gets Python None → falls through as Ok{modified=false}. Then content is still processed. + // The real observable difference: with non-None structuredContent, verify it IS found. + // Also verify None structuredContent is NOT treated as present (struct_key is None). + #[test] + fn mcp_content_dict_nonnone_structured_content_sets_structured_content_processed() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(1000), "truncate").unwrap(); + let sc = PyDict::new(py); + sc.set_item("data", "value").unwrap(); + let item = PyDict::new(py); + item.set_item("type", "text").unwrap(); + item.set_item("text", "short").unwrap(); + let content = PyList::new(py, [item]).unwrap(); + let result_dict = PyDict::new(py); + result_dict.set_item("content", content).unwrap(); + result_dict.set_item("structuredContent", &sc).unwrap(); + let payload = make_payload(py, "t", result_dict.as_any().clone()).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let result = result.bind(py); + let meta = result.getattr("metadata").unwrap(); + let meta_dict = meta.cast::().unwrap(); + let sc_processed: bool = meta_dict + .get_item("structured_content_processed") + .unwrap() + .map(|v| v.extract::().unwrap_or(false)) + .unwrap_or(false); + assert!(sc_processed, "non-None structuredContent must be detected"); + }); + } + + #[test] + fn mcp_content_dict_none_structured_content_does_not_set_structured_content_processed() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(1000), "truncate").unwrap(); + let item = PyDict::new(py); + item.set_item("type", "text").unwrap(); + item.set_item("text", "short").unwrap(); + let content = PyList::new(py, [item]).unwrap(); + let result_dict = PyDict::new(py); + result_dict.set_item("content", content).unwrap(); + // structuredContent = None → must NOT be treated as present + result_dict + .set_item("structuredContent", py.None()) + .unwrap(); + let payload = make_payload(py, "t", result_dict.as_any().clone()).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let result = result.bind(py); + let meta = result.getattr("metadata").unwrap(); + let meta_dict = meta.cast::().unwrap(); + // sc_processed = (struct_key.is_some()) = false when structuredContent is None + let sc_processed: bool = meta_dict + .get_item("structured_content_processed") + .unwrap() + .map(|v| v.extract::().unwrap_or(true)) + .unwrap_or(true); + assert!( + !sc_processed, + "None structuredContent must NOT be treated as present; ! deletion mutant would set sc_processed=true" + ); + }); + } + + #[test] + fn token_mode_metrics_emit_correct_limit_mode() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let d = PyDict::new(py); + d.set_item("max_tokens", 2usize).unwrap(); // 2 tokens * 4 chars = 8 chars max + d.set_item("limit_mode", "token").unwrap(); + d.set_item("strategy", "truncate").unwrap(); + d.set_item("max_chars", py.None()).unwrap(); + let core = OutputLengthGuardPluginCore::new(d.as_any()).unwrap(); + // 16 chars = 4 estimated tokens > 2 → truncation fires + let text = "abcdefghijklmnop".into_pyobject(py).unwrap().into_any(); + let payload = make_payload(py, "t", text).unwrap(); + let ctx = PyDict::new(py); + let ext = make_extensions(py, "trace-token").unwrap(); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), Some(&ext)) + .unwrap(); + let result = result.bind(py); + let meta = result.getattr("metadata").unwrap(); + let inner = meta.get_item("output_length_guard").unwrap(); + assert!(!inner.is_none(), "metrics must be present with trace_id"); + let limit_mode: String = inner.get_item("limit_mode").unwrap().extract().unwrap(); + assert_eq!( + limit_mode, "token", + "token-mode plugin must emit limit_mode='token', not 'character'" + ); + let strategy: String = inner.get_item("strategy").unwrap().extract().unwrap(); + assert_eq!( + strategy, "truncate", + "truncate-strategy plugin must emit strategy='truncate'" + ); + }); + } + + #[test] + fn mcp_content_dict_oversized_list_truncate_mode_passes_through_unchanged() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let d = PyDict::new(py); + d.set_item("max_chars", py.None()).unwrap(); // no char limit + d.set_item("max_structure_size", 2usize).unwrap(); + d.set_item("strategy", "truncate").unwrap(); + d.set_item("limit_mode", "character").unwrap(); + let core = OutputLengthGuardPluginCore::new(d.as_any()).unwrap(); + // 3 items > max_structure_size=2, strategy=truncate: must NOT block, + // must return the list unchanged (no panic, no infinite loop). + let item_a = PyDict::new(py); + item_a.set_item("type", "text").unwrap(); + item_a.set_item("text", "a").unwrap(); + let item_b = PyDict::new(py); + item_b.set_item("type", "text").unwrap(); + item_b.set_item("text", "b").unwrap(); + let item_c = PyDict::new(py); + item_c.set_item("type", "text").unwrap(); + item_c.set_item("text", "c").unwrap(); + let content = PyList::new(py, [item_a, item_b, item_c]).unwrap(); + let result_dict = PyDict::new(py); + result_dict.set_item("content", content).unwrap(); + let payload = make_payload(py, "t", result_dict.as_any().clone()).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let cp: bool = result + .bind(py) + .getattr("continue_processing") + .unwrap() + .extract() + .unwrap(); + assert!( + cp, + "oversized list with truncate strategy must not block (continue_processing=true)" + ); + }); + } + + #[test] + fn char_mode_detection_uses_char_count_not_bytes_for_cjk() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + // "你好世界" = 4 CJK chars, 12 UTF-8 bytes + let core = make_core(Some(10), "block").unwrap(); + let text = "你好世界".into_pyobject(py).unwrap().into_any(); + let payload = make_payload(py, "t", text).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let cp: bool = result + .bind(py) + .getattr("continue_processing") + .unwrap() + .extract() + .unwrap(); + assert!( + cp, + "4-char CJK string must not be blocked with max_chars=10 (char count=4, byte len=12)" + ); + // modified_payload must be None — no truncation needed + let modified = result.bind(py).getattr("modified_payload").unwrap(); + assert!( + modified.is_none(), + "4-char CJK string must pass through unmodified" + ); + }); + } + + // Companion: 11-char CJK string (33 bytes) with max_chars=10 MUST block. + #[test] + fn char_mode_detection_blocks_cjk_string_exceeding_char_limit() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + // "你好世界!" × 3 = 15 chars, well above max_chars=10 + let core = make_core(Some(10), "block").unwrap(); + let text = "你好世界!你好世界!你" + .into_pyobject(py) + .unwrap() + .into_any(); // 11 chars + let payload = make_payload(py, "t", text).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let cp: bool = result + .bind(py) + .getattr("continue_processing") + .unwrap() + .extract() + .unwrap(); + assert!(!cp, "11-char CJK string must be blocked with max_chars=10"); + }); + } + + #[test] + fn mcp_content_dict_oversized_list_truncate_mode_still_guards_item_text() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let d = PyDict::new(py); + d.set_item("max_chars", 2usize).unwrap(); // force truncation + d.set_item("max_structure_size", 2usize).unwrap(); + d.set_item("strategy", "truncate").unwrap(); + d.set_item("limit_mode", "character").unwrap(); + let core = OutputLengthGuardPluginCore::new(d.as_any()).unwrap(); + // 3 items > max_structure_size=2; strategy=truncate → items must still be guarded + let make_item = |text: &str| { + let item = PyDict::new(py); + item.set_item("type", "text").unwrap(); + item.set_item("text", text).unwrap(); + item + }; + let content = PyList::new( + py, + [ + make_item("abcdef"), + make_item("abcdef"), + make_item("abcdef"), + ], + ) + .unwrap(); + let result_dict = PyDict::new(py); + result_dict.set_item("content", content).unwrap(); + let payload = make_payload(py, "t", result_dict.as_any().clone()).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let result = result.bind(py); + // continue_processing must be true (truncate, not block) + let cp: bool = result + .getattr("continue_processing") + .unwrap() + .extract() + .unwrap(); + assert!(cp, "truncate mode must not block"); + // modified_payload must be present — items were truncated + let modified = result.getattr("modified_payload").unwrap(); + assert!( + !modified.is_none(), + "oversized truncate list: items must still be truncated" + ); + // Each item text must be within max_chars=2 + let modified_result = modified.getattr("result").unwrap(); + let modified_result_dict = modified_result.cast::().unwrap(); + let content_val = modified_result_dict.get_item("content").unwrap().unwrap(); + let content_out = content_val.cast::().unwrap(); + for item in content_out.iter() { + let text: String = item + .cast::() + .unwrap() + .get_item("text") + .unwrap() + .unwrap() + .extract() + .unwrap(); + assert!( + text.chars().count() <= 2, + "item text '{}' exceeds max_chars=2", + text + ); + } + }); + } + + #[test] + fn violation_carries_mcp_error_code_and_http_status_code() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let core = make_core(Some(5), "block").unwrap(); + let text = "this is a long string" + .into_pyobject(py) + .unwrap() + .into_any(); + let payload = make_payload(py, "t", text).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let result = result.bind(py); + let violation = result.getattr("violation").unwrap(); + assert!(!violation.is_none(), "expected a violation"); + let mcp_code: i64 = violation + .getattr("mcp_error_code") + .unwrap() + .extract() + .unwrap(); + assert_eq!(mcp_code, -32000, "mcp_error_code must be -32000"); + let http_code: i64 = violation + .getattr("http_status_code") + .unwrap() + .extract() + .unwrap(); + assert_eq!(http_code, 422, "http_status_code must be 422"); + }); + } + + #[test] + fn below_min_truncate_mode_reports_within_bounds_false() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + install_framework_module(py).unwrap(); + let d = PyDict::new(py); + d.set_item("min_chars", 20usize).unwrap(); // min > len("short") = 5 + d.set_item("max_chars", py.None()).unwrap(); + d.set_item("strategy", "truncate").unwrap(); + d.set_item("limit_mode", "character").unwrap(); + let core = OutputLengthGuardPluginCore::new(d.as_any()).unwrap(); + let text = "short".into_pyobject(py).unwrap().into_any(); // 5 chars < min_chars=20 + let payload = make_payload(py, "t", text).unwrap(); + let ctx = PyDict::new(py); + let result = core + .tool_post_invoke(py, &payload, ctx.as_any(), None) + .unwrap(); + let result = result.bind(py); + // Must pass through (no violation, no modification) + let cp: bool = result + .getattr("continue_processing") + .unwrap() + .extract() + .unwrap(); + assert!(cp, "truncate mode must not block below-min strings"); + assert!( + result.getattr("modified_payload").unwrap().is_none(), + "below-min truncate must not modify the payload" + ); + // within_bounds must be false: value is out-of-bounds even though no truncation occurs + let meta = result.getattr("metadata").unwrap(); + let within_bounds: bool = meta.get_item("within_bounds").unwrap().extract().unwrap(); + assert!( + !within_bounds, + "below_min truncate mode must report within_bounds=false" + ); + }); + } +} diff --git a/plugins/rust/python-package/output_length_guard/src/structured.rs b/plugins/rust/python-package/output_length_guard/src/structured.rs new file mode 100644 index 0000000..e256951 --- /dev/null +++ b/plugins/rust/python-package/output_length_guard/src/structured.rs @@ -0,0 +1,976 @@ +// Copyright 2025 +// SPDX-License-Identifier: Apache-2.0 +// +// Structured data processing for output length guard + +use pyo3::prelude::*; +use pyo3::types::{PyDict, PyList}; + +use crate::config::{OutputLengthGuardConfig, Strategy}; +use crate::guards::{evaluate_text_limits, is_numeric_string, truncate}; + +/// Result of processing a structured value. +pub enum ProcessResult { + /// No violation, data optionally modified. + Ok { value: Py, modified: bool }, + /// A policy violation was detected. + Violation { + reason: String, + description: String, + code: String, + details: Vec<(String, serde_json::Value)>, + }, +} + +/// Recursively process structured data, applying length guard. +/// Mirrors Python _process_structured_data(). +pub fn process_structured_data( + py: Python<'_>, + data: &Bound<'_, PyAny>, + cfg: &OutputLengthGuardConfig, + path: &str, + depth: usize, +) -> PyResult { + // Security: Check recursion depth + if depth > cfg.max_recursion_depth { + log::error!( + "Recursion depth {} exceeds maximum {} at path: {}", + depth, + cfg.max_recursion_depth, + path + ); + if cfg.strategy == Strategy::Block { + return Ok(ProcessResult::Violation { + reason: "Recursion depth exceeds security limit".to_string(), + description: format!( + "Nesting depth {} exceeds limit of {}", + depth, cfg.max_recursion_depth + ), + code: "STRUCTURE_DEPTH_VIOLATION".to_string(), + details: vec![ + ("depth".to_string(), serde_json::json!(depth)), + ( + "max_depth".to_string(), + serde_json::json!(cfg.max_recursion_depth), + ), + ( + "location".to_string(), + serde_json::json!(if path.is_empty() { "root" } else { path }), + ), + ], + }); + } + return Ok(ProcessResult::Ok { + value: data.clone().unbind(), + modified: false, + }); + } + + // Base case: string + if let Ok(text) = data.extract::() { + return process_string(py, &text, cfg, path); + } + + // Recursive case: list + if let Ok(list) = data.cast::() { + return process_list(py, list, cfg, path, depth); + } + + // Recursive case: dict + if let Ok(dict) = data.cast::() { + return process_dict(py, dict, cfg, path, depth); + } + + // Other types (int, bool, None, etc.) — pass through + Ok(ProcessResult::Ok { + value: data.clone().unbind(), + modified: false, + }) +} + +fn process_string( + py: Python<'_>, + text: &str, + cfg: &OutputLengthGuardConfig, + path: &str, +) -> PyResult { + if is_numeric_string(text) { + return Ok(ProcessResult::Ok { + value: text.into_pyobject(py)?.into_any().unbind(), + modified: false, + }); + } + + // Use char count (not byte length) so that max_chars is enforced in Unicode + // codepoints, matching Python's len(str) semantics. Byte length is only used + // for token estimation, which mirrors Python's len(text) // chars_per_token. + let char_count = text.chars().count(); + let token_count = text.len() / cfg.chars_per_token.max(1); + let (below_min, above_max) = evaluate_text_limits(char_count, token_count, cfg); + + if !below_min && !above_max { + return Ok(ProcessResult::Ok { + value: text.into_pyobject(py)?.into_any().unbind(), + modified: false, + }); + } + + let location = if path.is_empty() { "root" } else { path }; + + if cfg.strategy == Strategy::Block { + let violation = if above_max && cfg.limit_mode == crate::config::LimitMode::Token { + ProcessResult::Violation { + reason: format!("Estimated token count out of bounds at {}", location), + description: format!( + "Estimated token count {} exceeds max_tokens {} at {}", + token_count, + cfg.max_tokens.unwrap_or(0), + location + ), + code: "OUTPUT_TOKEN_VIOLATION".to_string(), + details: vec![ + ("token_count".to_string(), serde_json::json!(token_count)), + ("max_tokens".to_string(), serde_json::json!(cfg.max_tokens)), + ( + "chars_per_token".to_string(), + serde_json::json!(cfg.chars_per_token), + ), + ( + "strategy".to_string(), + serde_json::json!(cfg.strategy.as_str()), + ), + ("location".to_string(), serde_json::json!(location)), + ], + } + } else if above_max { + ProcessResult::Violation { + reason: format!("String length out of bounds at {}", location), + description: format!( + "String length {} exceeds max_chars {} at {}", + char_count, + cfg.max_chars.unwrap_or(0), + location + ), + code: "OUTPUT_LENGTH_VIOLATION".to_string(), + details: vec![ + ("length".to_string(), serde_json::json!(char_count)), + ("max_chars".to_string(), serde_json::json!(cfg.max_chars)), + ( + "strategy".to_string(), + serde_json::json!(cfg.strategy.as_str()), + ), + ("location".to_string(), serde_json::json!(location)), + ], + } + } else { + // below_min + ProcessResult::Violation { + reason: format!("String length/tokens below minimum at {}", location), + description: format!( + "String length {} or tokens {} below minimum at {}", + char_count, token_count, location + ), + code: "OUTPUT_LENGTH_VIOLATION".to_string(), + details: vec![ + ("length".to_string(), serde_json::json!(char_count)), + ("min_chars".to_string(), serde_json::json!(cfg.min_chars)), + ("token_count".to_string(), serde_json::json!(token_count)), + ("min_tokens".to_string(), serde_json::json!(cfg.min_tokens)), + ("location".to_string(), serde_json::json!(location)), + ], + } + }; + return Ok(violation); + } + + // Truncate mode: only truncate if above_max + if above_max { + let truncated = truncate(text, cfg); + let modified = truncated != text; + let value = truncated.into_pyobject(py)?.into_any().unbind(); + return Ok(ProcessResult::Ok { value, modified }); + } + + // Below min in truncate mode — allow through unchanged + Ok(ProcessResult::Ok { + value: text.into_pyobject(py)?.into_any().unbind(), + modified: false, + }) +} + +fn process_list( + py: Python<'_>, + list: &Bound<'_, PyList>, + cfg: &OutputLengthGuardConfig, + path: &str, + depth: usize, +) -> PyResult { + if list.len() > cfg.max_structure_size { + log::error!( + "List size {} exceeds maximum {} at path: {}", + list.len(), + cfg.max_structure_size, + path + ); + if cfg.strategy == Strategy::Block { + return Ok(ProcessResult::Violation { + reason: "Structure size exceeds security limit".to_string(), + description: format!( + "List has {} items, exceeding limit of {}", + list.len(), + cfg.max_structure_size + ), + code: "STRUCTURE_SIZE_VIOLATION".to_string(), + details: vec![ + ("size".to_string(), serde_json::json!(list.len())), + ( + "max_size".to_string(), + serde_json::json!(cfg.max_structure_size), + ), + ( + "location".to_string(), + serde_json::json!(if path.is_empty() { "root" } else { path }), + ), + ], + }); + } + return Ok(ProcessResult::Ok { + value: list.clone().into_any().unbind(), + modified: false, + }); + } + + let mut modified = false; + let out_list = PyList::empty(py); + + for (idx, item) in list.iter().enumerate() { + let item_path = if path.is_empty() { + format!("[{}]", idx) + } else { + format!("{}[{}]", path, idx) + }; + + match process_structured_data(py, &item, cfg, &item_path, depth + 1)? { + v @ ProcessResult::Violation { .. } => return Ok(v), + ProcessResult::Ok { + value, + modified: item_modified, + } => { + out_list.append(value.bind(py))?; + if item_modified { + modified = true; + } + } + } + } + + Ok(ProcessResult::Ok { + value: out_list.into_any().unbind(), + modified, + }) +} + +fn process_dict( + py: Python<'_>, + dict: &Bound<'_, PyDict>, + cfg: &OutputLengthGuardConfig, + path: &str, + depth: usize, +) -> PyResult { + if dict.len() > cfg.max_structure_size { + log::error!( + "Dict size {} exceeds maximum {} at path: {}", + dict.len(), + cfg.max_structure_size, + path + ); + if cfg.strategy == Strategy::Block { + return Ok(ProcessResult::Violation { + reason: "Structure size exceeds security limit".to_string(), + description: format!( + "Dict has {} items, exceeding limit of {}", + dict.len(), + cfg.max_structure_size + ), + code: "STRUCTURE_SIZE_VIOLATION".to_string(), + details: vec![ + ("size".to_string(), serde_json::json!(dict.len())), + ( + "max_size".to_string(), + serde_json::json!(cfg.max_structure_size), + ), + ( + "location".to_string(), + serde_json::json!(if path.is_empty() { "root" } else { path }), + ), + ], + }); + } + return Ok(ProcessResult::Ok { + value: dict.clone().into_any().unbind(), + modified: false, + }); + } + + let mut modified = false; + let out_dict = PyDict::new(py); + + for (key, value) in dict.iter() { + let key_str = key.extract::().unwrap_or_default(); + let value_path = if path.is_empty() { + key_str.clone() + } else { + format!("{}.{}", path, key_str) + }; + + match process_structured_data(py, &value, cfg, &value_path, depth + 1)? { + v @ ProcessResult::Violation { .. } => return Ok(v), + ProcessResult::Ok { + value: new_val, + modified: val_modified, + } => { + out_dict.set_item(&key, new_val.bind(py))?; + if val_modified { + modified = true; + } + } + } + } + + Ok(ProcessResult::Ok { + value: out_dict.into_any().unbind(), + modified, + }) +} + +/// Generate a text representation of structured data. +/// Mirrors Python _generate_text_representation(). +pub fn generate_text_representation(data: &Bound<'_, PyAny>, depth: usize) -> PyResult { + if let Ok(s) = data.extract::() { + return Ok(s); + } + + // Single-key dict unwrapping with depth limit + if let Ok(dict) = data.cast::() { + if dict.len() == 1 + && depth < 10 + && let Some((_, val)) = dict.iter().next() + { + return generate_text_representation(&val, depth + 1); + } + // Multi-key dict or depth limit reached + return json_dumps(data); + } + + if data.cast::().is_ok() { + return json_dumps(data); + } + + // Fallback to repr() + Ok(data.repr()?.to_string()) +} + +fn json_dumps(data: &Bound<'_, PyAny>) -> PyResult { + let py = data.py(); + let json_module = pyo3::types::PyModule::import(py, "json")?; + let result = json_module.getattr("dumps")?.call( + (data,), + Some(&{ + let kw = PyDict::new(py); + kw.set_item("ensure_ascii", false)?; + kw.set_item("separators", (",", ":"))?; + kw + }), + )?; + result.extract::() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::{LimitMode, OutputLengthGuardConfig, Strategy}; + use pyo3::types::{PyDict, PyList}; + + fn block_char_cfg(max_chars: usize) -> OutputLengthGuardConfig { + OutputLengthGuardConfig { + max_chars: Some(max_chars), + limit_mode: LimitMode::Character, + strategy: Strategy::Block, + ellipsis: "…".to_string(), + ..Default::default() + } + } + + fn truncate_char_cfg(max_chars: usize) -> OutputLengthGuardConfig { + OutputLengthGuardConfig { + max_chars: Some(max_chars), + limit_mode: LimitMode::Character, + strategy: Strategy::Truncate, + ellipsis: "…".to_string(), + ..Default::default() + } + } + + #[test] + fn process_string_within_limit_unchanged() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let cfg = truncate_char_cfg(100); + let s = "hello world".into_pyobject(py).unwrap().into_any(); + match process_structured_data(py, &s, &cfg, "", 0).unwrap() { + ProcessResult::Ok { modified, value } => { + assert!(!modified); + assert_eq!(value.bind(py).extract::().unwrap(), "hello world"); + } + ProcessResult::Violation { .. } => panic!("unexpected violation"), + } + }); + } + + #[test] + fn process_string_truncates_when_over_limit() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let cfg = truncate_char_cfg(5); + let s = "hello world".into_pyobject(py).unwrap().into_any(); + match process_structured_data(py, &s, &cfg, "", 0).unwrap() { + ProcessResult::Ok { modified, value } => { + assert!(modified); + let result = value.bind(py).extract::().unwrap(); + assert!(result.chars().count() <= 5); + } + ProcessResult::Violation { .. } => panic!("unexpected violation"), + } + }); + } + + #[test] + fn process_string_blocks_when_over_limit_in_block_mode() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let cfg = block_char_cfg(5); + let s = "hello world".into_pyobject(py).unwrap().into_any(); + match process_structured_data(py, &s, &cfg, "", 0).unwrap() { + ProcessResult::Violation { code, .. } => { + assert_eq!(code, "OUTPUT_LENGTH_VIOLATION"); + } + ProcessResult::Ok { .. } => panic!("expected violation"), + } + }); + } + + #[test] + fn process_string_skips_numeric_strings() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let cfg = block_char_cfg(3); // "123" is 3 chars, would pass but it's numeric + let s = "123".into_pyobject(py).unwrap().into_any(); + match process_structured_data(py, &s, &cfg, "", 0).unwrap() { + ProcessResult::Ok { modified, .. } => assert!(!modified), + ProcessResult::Violation { .. } => panic!("numeric string should pass through"), + } + }); + } + + #[test] + fn process_list_processes_each_element() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let cfg = truncate_char_cfg(5); + let list = PyList::new(py, ["hello world", "short"]).unwrap(); + match process_structured_data(py, list.as_any(), &cfg, "", 0).unwrap() { + ProcessResult::Ok { modified, value } => { + assert!(modified); + let out: Vec = value + .bind(py) + .cast::() + .unwrap() + .iter() + .map(|i| i.extract().unwrap()) + .collect(); + assert!(out[0].chars().count() <= 5); + assert_eq!(out[1], "short"); + } + ProcessResult::Violation { .. } => panic!("unexpected violation"), + } + }); + } + + #[test] + fn process_list_blocks_on_violation() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let cfg = block_char_cfg(3); + let list = PyList::new(py, ["hi", "hello world"]).unwrap(); + match process_structured_data(py, list.as_any(), &cfg, "", 0).unwrap() { + ProcessResult::Violation { code, .. } => { + assert_eq!(code, "OUTPUT_LENGTH_VIOLATION"); + } + ProcessResult::Ok { .. } => panic!("expected violation"), + } + }); + } + + #[test] + fn process_list_rejects_oversized_list_in_block_mode() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let mut cfg = truncate_char_cfg(1000); + cfg.max_structure_size = 2; + cfg.strategy = Strategy::Block; + let list = PyList::new(py, ["a", "b", "c"]).unwrap(); + match process_structured_data(py, list.as_any(), &cfg, "", 0).unwrap() { + ProcessResult::Violation { code, .. } => { + assert_eq!(code, "STRUCTURE_SIZE_VIOLATION"); + } + ProcessResult::Ok { .. } => panic!("expected violation"), + } + }); + } + + #[test] + fn process_dict_processes_text_values() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let cfg = truncate_char_cfg(5); + let d = PyDict::new(py); + d.set_item("key", "hello world").unwrap(); + match process_structured_data(py, d.as_any(), &cfg, "", 0).unwrap() { + ProcessResult::Ok { modified, value } => { + assert!(modified); + let out = value.bind(py).cast::().unwrap(); + let v: String = out.get_item("key").unwrap().unwrap().extract().unwrap(); + assert!(v.chars().count() <= 5); + } + ProcessResult::Violation { .. } => panic!("unexpected violation"), + } + }); + } + + #[test] + fn process_dict_rejects_oversized_dict_in_block_mode() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let mut cfg = truncate_char_cfg(1000); + cfg.max_structure_size = 1; + cfg.strategy = Strategy::Block; + let d = PyDict::new(py); + d.set_item("a", "v1").unwrap(); + d.set_item("b", "v2").unwrap(); + match process_structured_data(py, d.as_any(), &cfg, "", 0).unwrap() { + ProcessResult::Violation { code, .. } => { + assert_eq!(code, "STRUCTURE_SIZE_VIOLATION"); + } + ProcessResult::Ok { .. } => panic!("expected violation"), + } + }); + } + + #[test] + fn process_depth_exceeded_blocks_in_block_mode() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let mut cfg = truncate_char_cfg(1000); + cfg.max_recursion_depth = 0; + cfg.strategy = Strategy::Block; + let d = PyDict::new(py); + d.set_item("x", "value").unwrap(); + match process_structured_data(py, d.as_any(), &cfg, "", 1).unwrap() { + ProcessResult::Violation { code, .. } => { + assert_eq!(code, "STRUCTURE_DEPTH_VIOLATION"); + } + ProcessResult::Ok { .. } => panic!("expected violation"), + } + }); + } + + #[test] + fn integers_pass_through_unchanged() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let cfg = block_char_cfg(1); + let n = 42_i64.into_pyobject(py).unwrap().into_any(); + match process_structured_data(py, &n, &cfg, "", 0).unwrap() { + ProcessResult::Ok { modified, .. } => assert!(!modified), + ProcessResult::Violation { .. } => panic!("int should pass through"), + } + }); + } + + // ── recursion depth boundary ────────────────────────────────────────── + // Kill: replace > with >= (depth == max_recursion_depth must NOT fire) + #[test] + fn process_depth_at_exactly_limit_does_not_block() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let mut cfg = truncate_char_cfg(1000); + cfg.max_recursion_depth = 5; + cfg.strategy = Strategy::Block; + // Pass a plain string (not a dict/list) so no recursive calls are made. + // At depth=5, max=5: 5 > 5 is false → must NOT violate. + let s = "hello".into_pyobject(py).unwrap().into_any(); + match process_structured_data(py, &s, &cfg, "", 5).unwrap() { + ProcessResult::Ok { .. } => {} + ProcessResult::Violation { code, .. } => { + panic!("depth == limit must not block, got code={}", code) + } + } + }); + } + + // ── process_string token calculation ───────────────────────────────── + // Kill: replace / with % or * in token_count calculation (line 105) + #[test] + fn process_string_token_mode_truncates_by_estimated_tokens() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let mut cfg = OutputLengthGuardConfig { + max_tokens: Some(2), + limit_mode: LimitMode::Token, + strategy: Strategy::Block, + chars_per_token: 4, + ellipsis: "…".to_string(), + ..Default::default() + }; + cfg.max_chars = None; + // "abcdefghijklmno" = 15 chars → estimated tokens = 15/4 = 3 > 2 → violation + let s = "abcdefghijklmno".into_pyobject(py).unwrap().into_any(); + match process_structured_data(py, &s, &cfg, "", 0).unwrap() { + ProcessResult::Violation { code, .. } => assert_eq!(code, "OUTPUT_TOKEN_VIOLATION"), + ProcessResult::Ok { .. } => panic!("expected token violation"), + } + }); + } + + // Kill: replace / with % in token_count = length / cpt. + // length=9, cpt=4, max_tokens=1: + // / → 9/4=2 > 1 → violation ✓ + // % → 9%4=1 > 1 → false → no violation ✗ (mutant survives without this test) + #[test] + fn process_string_token_mode_modulo_mutant_is_killed() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let cfg = OutputLengthGuardConfig { + max_tokens: Some(1), + limit_mode: LimitMode::Token, + strategy: Strategy::Block, + chars_per_token: 4, + max_chars: None, + ellipsis: "…".to_string(), + ..Default::default() + }; + // length=9: 9/4=2 > 1 → violation; 9%4=1 is not > 1 → would pass through + let s = "abcdefghi".into_pyobject(py).unwrap().into_any(); // 9 chars + match process_structured_data(py, &s, &cfg, "", 0).unwrap() { + ProcessResult::Violation { code, .. } => { + assert_eq!( + code, "OUTPUT_TOKEN_VIOLATION", + "9/4=2 > max_tokens=1 must violate" + ) + } + ProcessResult::Ok { .. } => { + panic!( + "expected token violation: 9/4=2 > max_tokens=1; % mutant gives 9%4=1 which would pass" + ) + } + } + }); + } + + // Kill: replace / with * in token_count = length / cpt. + // length=4, cpt=4, max_tokens=1: + // / → 4/4=1 > 1 → false → no violation ✓ + // * → 4*4=16 > 1 → violation ✗ (mutant would block) + #[test] + fn process_string_token_mode_multiply_mutant_is_killed() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let cfg = OutputLengthGuardConfig { + max_tokens: Some(1), + limit_mode: LimitMode::Token, + strategy: Strategy::Block, + chars_per_token: 4, + max_chars: None, + ellipsis: "…".to_string(), + ..Default::default() + }; + // length=4: 4/4=1, NOT > 1 → no violation; 4*4=16 > 1 → would block + let s = "abcd".into_pyobject(py).unwrap().into_any(); // exactly 1 token + match process_structured_data(py, &s, &cfg, "", 0).unwrap() { + ProcessResult::Ok { modified, .. } => { + assert!( + !modified, + "4 chars = 1 token = max_tokens: must not block or modify" + ); + } + ProcessResult::Violation { .. } => { + panic!("4/4=1 is not > max_tokens=1; * mutant gives 4*4=16 which would block"); + } + } + }); + } + + // Kill: delete ! in `if !below_min && !above_max` (line 108) + #[test] + fn process_string_within_both_limits_passes_through_unmodified() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let mut cfg = truncate_char_cfg(100); + cfg.min_chars = 2; + // "hello" = 5 chars, between min=2 and max=100 → must pass through unchanged + let s = "hello".into_pyobject(py).unwrap().into_any(); + match process_structured_data(py, &s, &cfg, "", 0).unwrap() { + ProcessResult::Ok { modified, value } => { + assert!(!modified); + assert_eq!(value.bind(py).extract::().unwrap(), "hello"); + } + ProcessResult::Violation { .. } => panic!("string within bounds must not violate"), + } + }); + } + + // ── list/dict size boundary (> vs >=) ──────────────────────────────── + // Kill: replace > with >= in process_list size check (line 205) + #[test] + fn process_list_at_exactly_max_structure_size_is_not_oversized() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let mut cfg = truncate_char_cfg(1000); + cfg.max_structure_size = 3; + cfg.strategy = Strategy::Block; + // Exactly 3 items == max_structure_size: must NOT trigger violation + let list = PyList::new(py, ["a", "b", "c"]).unwrap(); + match process_structured_data(py, list.as_any(), &cfg, "", 0).unwrap() { + ProcessResult::Ok { .. } => {} + ProcessResult::Violation { code, .. } => { + panic!("list.len() == max must not block, got code={}", code) + } + } + }); + } + + // Kill: replace > with >= in process_dict size check (line 277) + #[test] + fn process_dict_at_exactly_max_structure_size_is_not_oversized() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let mut cfg = truncate_char_cfg(1000); + cfg.max_structure_size = 2; + cfg.strategy = Strategy::Block; + let d = PyDict::new(py); + d.set_item("a", "v1").unwrap(); + d.set_item("b", "v2").unwrap(); + // Exactly 2 keys == max_structure_size: must NOT trigger violation + match process_structured_data(py, d.as_any(), &cfg, "", 0).unwrap() { + ProcessResult::Ok { .. } => {} + ProcessResult::Violation { code, .. } => { + panic!("dict.len() == max must not block, got code={}", code) + } + } + }); + } + + // ── list/dict depth increments (+ 1 vs * 1 mutation) ───────────────── + // Kill: replace + with * in `depth + 1` recursive calls (lines 250, 323) + #[test] + fn process_list_recurses_correctly_into_nested_string() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let cfg = block_char_cfg(3); + // Nested: list containing a string that exceeds the limit + let list = PyList::new(py, ["toolongstring"]).unwrap(); + match process_structured_data(py, list.as_any(), &cfg, "", 0).unwrap() { + ProcessResult::Violation { code, .. } => { + assert_eq!(code, "OUTPUT_LENGTH_VIOLATION") + } + ProcessResult::Ok { .. } => panic!("expected violation from nested string"), + } + }); + } + + #[test] + fn process_dict_recurses_correctly_into_nested_string() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let cfg = block_char_cfg(3); + let d = PyDict::new(py); + d.set_item("k", "toolongstring").unwrap(); + match process_structured_data(py, d.as_any(), &cfg, "", 0).unwrap() { + ProcessResult::Violation { code, .. } => { + assert_eq!(code, "OUTPUT_LENGTH_VIOLATION") + } + ProcessResult::Ok { .. } => panic!("expected violation from nested string"), + } + }); + } + + // ── generate_text_representation ───────────────────────────────────── + // Kill: replace == with != in `if dict.len() == 1` (line 352) + #[test] + fn generate_text_representation_single_key_dict_unwraps_value() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let d = PyDict::new(py); + d.set_item("key", "hello").unwrap(); + let result = generate_text_representation(d.as_any(), 0).unwrap(); + assert_eq!(result, "hello"); + }); + } + + // Kill: replace == with != (multi-key dict must NOT unwrap) + #[test] + fn generate_text_representation_multi_key_dict_json_serialises() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let d = PyDict::new(py); + d.set_item("a", "v1").unwrap(); + d.set_item("b", "v2").unwrap(); + let result = generate_text_representation(d.as_any(), 0).unwrap(); + // Multi-key: serialised as JSON, not unwrapped + assert!(result.contains("v1") && result.contains("v2")); + }); + } + + // Kill: replace < with == / > / <= in `depth < 10` unwrap guard (line 353) + #[test] + fn generate_text_representation_stops_unwrapping_at_depth_10() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + // At depth=10, a single-key dict must NOT be unwrapped (depth < 10 is false) + let d = PyDict::new(py); + d.set_item("key", "hello").unwrap(); + let result = generate_text_representation(d.as_any(), 10).unwrap(); + // Must be JSON, not the raw "hello" string + assert!(result.contains("key") || result.contains("hello")); + // More importantly: it must NOT equal the bare unwrapped value when depth==10 + assert_ne!(result, "hello", "must not unwrap at depth==10"); + }); + } + + // Kill: replace + with - in `depth + 1` recursive call (line 356) + #[test] + fn generate_text_representation_depth_9_still_unwraps() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + // At depth=9, unwrapping is still allowed (9 < 10) + let d = PyDict::new(py); + d.set_item("k", "leaf").unwrap(); + let result = generate_text_representation(d.as_any(), 9).unwrap(); + assert_eq!(result, "leaf"); + }); + } + + // ── process_list depth+1 vs depth*1 (structured.rs:250) ───────────────── + // Kill: replace + with * in `depth + 1` inside process_list recursive call. + // With depth*1, the depth never increments, so max_recursion_depth is never reached. + // Strategy: set max_recursion_depth=1, build list-inside-list (depth 0 → 1 → must hit 2). + // With correct depth+1: outer call at depth=0, list processes items at depth=1. + // Items at depth=1 are strings → process_string at depth=1. But depth check is + // `if depth > max_recursion_depth` at the TOP of process_structured_data. + // depth=1 > max=1 → false (doesn't fire). We need depth=2 to trigger. + // Use max_recursion_depth=1, and a list containing another list containing a string. + // Outer list: depth=0 → items processed at depth=1. + // Inner list: depth=1 → items processed at depth=2. depth=2 > max=1 → fires! + // With depth*1: items always processed at depth=0 (or depth*1=depth=0 since initial depth=0). + // Actually depth*1 = depth. So if initial depth=0: 0*1=0, never increments. + // Items in inner list would be processed at depth=0, so no depth violation. + #[test] + fn process_list_depth_increments_catch_deeply_nested_list() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let mut cfg = block_char_cfg(10000); // no char limit + cfg.max_recursion_depth = 1; + // Build: [["short_string"]] — depth 0 → list → depth 1 → list → depth 2 > 1 → violation + let inner_list = PyList::new(py, ["short_string"]).unwrap(); + let outer_list = PyList::new(py, [inner_list]).unwrap(); + // With correct depth+1: outer processes items at depth=1; inner list at depth=1 + // processes strings at depth=2; depth=2 > max=1 → STRUCTURE_DEPTH_VIOLATION. + // With depth*1 (depth never increments): always at depth=0, never fires. + match process_structured_data(py, outer_list.as_any(), &cfg, "", 0).unwrap() { + ProcessResult::Violation { code, .. } => { + assert_eq!( + code, "STRUCTURE_DEPTH_VIOLATION", + "expected depth violation from nested list, got: {}", + code + ); + } + ProcessResult::Ok { .. } => { + panic!("nested list beyond max_recursion_depth must produce a depth violation") + } + } + }); + } + + // ── process_dict depth+1 vs depth*1 (structured.rs:323) ───────────────── + // Same as above but using nested dicts. + #[test] + fn process_dict_depth_increments_catch_deeply_nested_dict() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + let mut cfg = block_char_cfg(10000); + cfg.max_recursion_depth = 1; + // Build: {"outer": {"inner": "value"}} — depth 0 → dict → depth 1 → dict → depth 2 > 1 → violation + let inner_dict = PyDict::new(py); + inner_dict.set_item("inner", "value").unwrap(); + let outer_dict = PyDict::new(py); + outer_dict.set_item("outer", inner_dict).unwrap(); + match process_structured_data(py, outer_dict.as_any(), &cfg, "", 0).unwrap() { + ProcessResult::Violation { code, .. } => { + assert_eq!( + code, "STRUCTURE_DEPTH_VIOLATION", + "expected depth violation from nested dict, got: {}", + code + ); + } + ProcessResult::Ok { .. } => { + panic!("nested dict beyond max_recursion_depth must produce a depth violation") + } + } + }); + } + + // ── generate_text_representation depth+1 vs depth*1 (structured.rs:356) ── + // Kill: replace + with * in the recursive `generate_text_representation(&val, depth + 1)`. + // With depth*1: depth = 0 always (since 0*1=0). A chain of 11 single-key dicts would + // never hit the depth<10 limit → infinite recursion or incorrect result. + // We verify correct behaviour by using exactly 11 levels of nesting: + // d1 = {"k": d2}, d2 = {"k": d3}, ..., d10 = {"k": d11}, d11 = {"k": "leaf"} + // With correct depth+1: unwrapping stops at depth=10 → json_dumps → contains "k"/"leaf". + // With depth*1: never increments, so all 11 levels are traversed → returns "leaf". + // BUT we verify from depth=0 that the FIRST single-key dict IS unwrapped (depth=0 < 10). + // What matters is what happens at depth=10 inside the chain. The test at depth=10 already + // covers that. For the +1 vs *1 mutation we need the RECURSIVE call to hit the limit. + // Test: build chain of 10 nested dicts (depth 0-9 each unwrap), leaf at d10. + // With +1: d0 calls d1 at depth=1, d1 calls d2 at depth=2, ..., d9 calls d10 at depth=10. + // At depth=10: 10 < 10 is false → json_dumps(d10) → contains "k". + // With *1: d0 calls d1 at depth=0*1=0 (stays 0 each time) → "leaf" returned forever. + // So at depth=0 with a 10-level chain: correct (+1) → JSON; mutant (*1) → "leaf". + #[test] + fn generate_text_representation_chain_of_10_stops_at_depth_limit() { + pyo3::Python::initialize(); + pyo3::Python::attach(|py| { + // Build a chain of 11 nested single-key dicts: each has key "k" pointing to the next. + // The innermost (11th) is {"k": "leaf"}. + // At depth=0, 10 dicts can be unwrapped (depths 0-9 are < 10). + // The 11th dict is reached at depth=10 → depth < 10 is false → json_dumps → not "leaf". + let mut current: pyo3::Py = { + let d = PyDict::new(py); + d.set_item("k", "leaf").unwrap(); + d.into_any().unbind() + }; + for _ in 0..10 { + let d = PyDict::new(py); + d.set_item("k", current.bind(py)).unwrap(); + current = d.into_any().unbind(); + } + // current is the outermost dict (10 wrappers + 1 leaf = 11 levels total) + let result = generate_text_representation(current.bind(py), 0).unwrap(); + // With correct +1: the 11th dict (at depth=10) is json_dumps'd → NOT "leaf". + // With *1 mutation: always at depth=0, unwraps all → returns "leaf". + assert_ne!( + result, "leaf", + "chain of 11 single-key dicts must not return bare 'leaf' (depth limit must fire at depth=10)" + ); + }); + } +} diff --git a/plugins/rust/python-package/output_length_guard/uv.lock b/plugins/rust/python-package/output_length_guard/uv.lock new file mode 100644 index 0000000..3ef6542 --- /dev/null +++ b/plugins/rust/python-package/output_length_guard/uv.lock @@ -0,0 +1,1513 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform != 'win32'", +] + +[options] +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer-span = "P10D" + +[[package]] +name = "annotated-doc" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "ansicon" +version = "1.89.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/e2/1c866404ddbd280efedff4a9f15abfe943cb83cde6e895022370f3a61f85/ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1", size = 67312, upload-time = "2019-04-29T20:23:57.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/f9/f1c10e223c7b56a38109a3f2eb4e7fe9a757ea3ed3a166754fb30f65e466/ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec", size = 63675, upload-time = "2019-04-29T20:23:53.83Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "blessed" +version = "1.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinxed" }, + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/1b/b7c3971c1b34e6fe32ffcb00be769769cbb8ccaf0678aa9bb6f23aea677a/blessed-1.48.0.tar.gz", hash = "sha256:5ed4c0d40d0121669ef949e4f23465982614eb821bd110d1d5a98ed97dea13d8", size = 14036135, upload-time = "2026-08-07T17:35:44.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/3e/6e6ba6c809688332d2f8450371e672c76f7e8e73574aba9dfe89b55eb900/blessed-1.48.0-py3-none-any.whl", hash = "sha256:c4ce01cba220f41d2ff244e9829cb4ef2390a26ace8ce1687b8bced1613676e5", size = 131267, upload-time = "2026-08-07T17:35:42.469Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" }, + { url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" }, + { url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569, upload-time = "2026-08-03T21:19:34.142Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907, upload-time = "2026-08-03T21:19:35.174Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" }, + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/31/4971872b3ed8715346231fb6eb4da8fcba65a4143c189db151ee28a2812b/charset_normalizer-3.5.0.tar.gz", hash = "sha256:49bd5feb59b0bf3cbf6ebcf4352e371c95b9da9bacd4449f8b64d0ad2c10a26e", size = 169295, upload-time = "2026-08-12T14:35:31.624Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/42/71e4e3bfe59202feef062c68487f54c6adf501cfbe087ecd93e3cd597fea/charset_normalizer-3.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e46a37ea7fcf9ae01d71b2e5ece19f1565987f3e308394b829197cbefc061f92", size = 349110, upload-time = "2026-08-12T14:32:07.832Z" }, + { url = "https://files.pythonhosted.org/packages/0f/dd/fd3386d0fbd358d3b5c7a2fa5bf312afe6159b04fafeb67d39fa971d7448/charset_normalizer-3.5.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cdfed4d7a59333c8220c67dd3be4e7a6c887b67453a64394022dcc919570add", size = 250773, upload-time = "2026-08-12T14:32:09.113Z" }, + { url = "https://files.pythonhosted.org/packages/17/ad/4901a66d6d3b17f1096725d7e50266132c16555aa6a70047fe1cf262b4b2/charset_normalizer-3.5.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9491f594859b68052edebd69e05fb045055a713b57a67974e6c1553b4e503c39", size = 240229, upload-time = "2026-08-12T14:32:10.43Z" }, + { url = "https://files.pythonhosted.org/packages/43/b7/1d790e0425e0f4c99e9b89a3956a94a6d2d0c6f01b2a5eca93d8f082d5ac/charset_normalizer-3.5.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:420b19411959eec115063229536788e6b32d0a7fa907d6b940317919120d702d", size = 280757, upload-time = "2026-08-12T14:32:11.628Z" }, + { url = "https://files.pythonhosted.org/packages/44/bb/4b8c8086c67636e52d6354ad17697f54a00b40041ca53dc765737e21709b/charset_normalizer-3.5.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a565303d118ea3b94a4b6c076bf568069726be414e43b06d58f7070b076ce11d", size = 276174, upload-time = "2026-08-12T14:32:12.808Z" }, + { url = "https://files.pythonhosted.org/packages/25/fa/690a11924c40c766d258d2b74d817cc5efe7bbcfceeedc5c0f35256d7524/charset_normalizer-3.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:815f143a91983ba3041bba066e492ae3c42de523fb1c699685a1abf3313b7d1b", size = 261808, upload-time = "2026-08-12T14:32:14.138Z" }, + { url = "https://files.pythonhosted.org/packages/53/2d/4a6945eff0c8f684e3f5b7b978644ab18ba9198da060dbaa1d9206bc6cc9/charset_normalizer-3.5.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c5e981a5ac8641381efe6f0029467500661616a530d27bc6eedfe45f840599f8", size = 259922, upload-time = "2026-08-12T14:32:15.485Z" }, + { url = "https://files.pythonhosted.org/packages/60/4d/10ac7e07bbf7ea569effeb9524e32f345f7e643800b20d538fb4706eab4e/charset_normalizer-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1a573e1e428f93908e79e04b349717f400e720f2f82285f0aaaf3ee0ff7f4c79", size = 252315, upload-time = "2026-08-12T14:32:16.764Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/1ddacf7aa7da12097f229a6a4e71a70200937a621b3617f6dc819fb99a66/charset_normalizer-3.5.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:32e6d56dd825205f81e5c45bcebb4df6a11fb2bbf4969a01ef156d6ced90c224", size = 240600, upload-time = "2026-08-12T14:32:17.956Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/293444d48c8d86fe51552262117134a9fc666d68cbbbc9b8c1b2b35a29be/charset_normalizer-3.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:30ae26a1adcd943690dcbbc47f28be762bae9e08ad7442b78c86b1c0dd5a626c", size = 280788, upload-time = "2026-08-12T14:32:19.1Z" }, + { url = "https://files.pythonhosted.org/packages/12/ed/0e34e40584f51eda38d4a5daf25fd8586366347efd4b2470dbf64710e778/charset_normalizer-3.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5f51a19dc52197a20218b05ec5336d0c6b3b09935f838724722032c8d45dc91a", size = 258425, upload-time = "2026-08-12T14:32:20.207Z" }, + { url = "https://files.pythonhosted.org/packages/72/b2/c1b1c27f6f0ef35b21a8bdb592854bbf7f219629db3477f74c0b1380e0ed/charset_normalizer-3.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e44bc2780516b3df986d6fe33103c7080cd9dcd5576fe3cb4b0f64309c8f22b", size = 277437, upload-time = "2026-08-12T14:32:21.345Z" }, + { url = "https://files.pythonhosted.org/packages/b2/bd/a1b7d959a37847675adb2f5978f81703306dd78df4fefb82ee5a1cc5e37f/charset_normalizer-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7faa47b56070b3dd6f4898ed28528843ab130d53266cb9948d9b1f3bb1a5c5e8", size = 262947, upload-time = "2026-08-12T14:32:22.613Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fd/a76a2d7e639bfc6a9c869371e34f28231eaca7d7126ec19895aa38eedb51/charset_normalizer-3.5.0-cp311-cp311-win32.whl", hash = "sha256:830c04a49998b5ed58c8b642c65b7b26419397f52392a64121ba9fd0e95e7f9f", size = 181313, upload-time = "2026-08-12T14:32:23.736Z" }, + { url = "https://files.pythonhosted.org/packages/97/84/6fc03e802578df41a2ab9b6a1f26657fb92e32285a603ad5852a4a4f68c1/charset_normalizer-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:8cb9b6892b53bd6d11fa4cde3dbee020b1f0b6656be1fbaa1ec0d4324a7839db", size = 206197, upload-time = "2026-08-12T14:32:24.938Z" }, + { url = "https://files.pythonhosted.org/packages/0a/27/7208360ff1901607359869fcc45ca0989d597f3e30777ba30c7254170587/charset_normalizer-3.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:2403b489c103e9a18c835863fc6dd54361355c8291d4cafdb37492b683440b9b", size = 184925, upload-time = "2026-08-12T14:32:26.123Z" }, + { url = "https://files.pythonhosted.org/packages/6d/3c/045ea64ea5a550870dd8ab60b2242870328d53f17d2be593b4f9f3121474/charset_normalizer-3.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:98820e1ceb25c6df7a80c4fd8efa59cb121f99bc7c4c1693ad94a2caff5b311d", size = 343861, upload-time = "2026-08-12T14:32:27.254Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1b/7502be709db899d5b4801509829188b3a5a10969411da9c846115a5f1b70/charset_normalizer-3.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:608553f476fca509537e804c4a71f5eb166ce63b75141f89c2c686ce1aa36956", size = 237550, upload-time = "2026-08-12T14:32:28.385Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ce/66392661375148d9455c17bee25509a54e28c39969e34befa48ec8777936/charset_normalizer-3.5.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6753de11eef42f1c321b26d682957d92c7f7bbce6530f34bbe0f9291dd37cc6f", size = 229673, upload-time = "2026-08-12T14:32:29.668Z" }, + { url = "https://files.pythonhosted.org/packages/6a/64/f58c32a8d4ecf55b82ee61ee9aa6a664d4afcd36c72feb4c926fd6fe9af8/charset_normalizer-3.5.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f76dc0a47f94cb9b69d86f01e477f4b0371ca70208b9ccea7e063c41eed9046", size = 260768, upload-time = "2026-08-12T14:32:30.891Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ea/36c90e59a96386174377e855479ec154221ef001e96637e0b23be92489c4/charset_normalizer-3.5.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c387c6bf91b4774e359a48a179e2872b8e8bf741e4fde06ba8d1665eb9a4760a", size = 257880, upload-time = "2026-08-12T14:32:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/23/35/5b85772eb82528ef22ba29487ad544a7049dfd27f35b1a5a55dbc0843048/charset_normalizer-3.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14f6904a3cf870abf044df3a8c4924ac6c8ef77e9896586fd37e73ae96cff2af", size = 247547, upload-time = "2026-08-12T14:32:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/b6/14/ba11a99c2a22ab04c2d5383a700b378cb463a78ab15f36444cabc10cd671/charset_normalizer-3.5.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cce46dd29d73e135e8087b96eb62a4aca6d69391b7f97808c6588ebed3178f3", size = 243326, upload-time = "2026-08-12T14:32:34.794Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4a/cadba3f2400b45aa1d62a4ae0298bf58a3b30b1158baf15c338c7ce5b601/charset_normalizer-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b476cdb63df22da2b91837593380be3ddbe406f36c506c1c91d80e7196b66288", size = 238820, upload-time = "2026-08-12T14:32:35.984Z" }, + { url = "https://files.pythonhosted.org/packages/47/41/d5188b9342d75b72c2b05d3ee373f01a691397e770f001ee05e3b37925f5/charset_normalizer-3.5.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1f56ce84b317ef2a59d7d3461891c7597c79247d2192bb8114c68a1a1debfcc0", size = 231661, upload-time = "2026-08-12T14:32:37.191Z" }, + { url = "https://files.pythonhosted.org/packages/13/5f/df38fa972c4e945c3d8cee2bc4e610613af522fd359c7dc7a74c419f0278/charset_normalizer-3.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9ce0f885239357379d92fd9a5fddbe20f0e30e0527c29ba69f8e99eeb1304a76", size = 261459, upload-time = "2026-08-12T14:32:38.369Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d7/043ff7720067a3beee05523465ac9c1c846c68b7884930dd483f72ee5ab6/charset_normalizer-3.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:96ae7ab5d8155fde927aa0864fbc8ba3cc4fde6d41ab0c7cea9d6012b4978603", size = 242300, upload-time = "2026-08-12T14:32:39.505Z" }, + { url = "https://files.pythonhosted.org/packages/19/f6/33980b7b802a048e546a6d9ad2ea783a6cf6b10a86aaccd10db462d8b913/charset_normalizer-3.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bf91921009025e96ce57a03ced6d14604fc3baf0530351638e9504a55da6fa3b", size = 259101, upload-time = "2026-08-12T14:32:41.02Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b7/0d19bde844bff9165377c1da9ef3c4792a4c24bd49b5b7094d9e6f6ab58b/charset_normalizer-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0b2e44e6d42d1a4ff78ccc219a93c5449105d10b16198d1aea581080df8073f9", size = 249246, upload-time = "2026-08-12T14:32:42.47Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cc/2c34fdfaacdf0e96e880ef562cbf80a9b5f8ea97e0dd9e57ba348a9c65cf/charset_normalizer-3.5.0-cp312-cp312-win32.whl", hash = "sha256:deb99535e9bf0bea8e274c6413eb939a21be35a3f492678dba4d5b1f4d70f142", size = 178025, upload-time = "2026-08-12T14:32:43.909Z" }, + { url = "https://files.pythonhosted.org/packages/76/d0/c34dbd1df23bcbdc1b5d2f48256340d72fa747f1eb03924a9d2fa35ed85b/charset_normalizer-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:e54dd1a66fa4bce0ccaf0db9dde336e49b3eec646dc4c1c0991279369d373a14", size = 200143, upload-time = "2026-08-12T14:32:45.254Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/4e6bf1465d60c3d8f488d5bde140d0cb91cd23ccab0dc2895cc6c6982047/charset_normalizer-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:b8ea208b304587d47931b36481342d20336e0d338ab052f8b4305926482598d6", size = 180046, upload-time = "2026-08-12T14:32:46.545Z" }, + { url = "https://files.pythonhosted.org/packages/1d/be/cc7b7b6fc41984902c0d31b06f5d9297e67705c1dae9352608e5540fad09/charset_normalizer-3.5.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:5c23fa4f6eccdd601949cb00f3988c01d64e671d8faba356397971077022e144", size = 211050, upload-time = "2026-08-12T14:32:47.81Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ae/e3ec8f17313609f43f7b323012fdb1ee37b83432277ca4eceba83e00366c/charset_normalizer-3.5.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:07f6f42b5a6325df35b458004fb5f9f29bf502d89287a33c7cdef3590e31de0f", size = 222768, upload-time = "2026-08-12T14:32:49.027Z" }, + { url = "https://files.pythonhosted.org/packages/c5/50/9f9c0d7ccc1512d49e27a0e7c12c58ec71dfe91698fa4326f058c33e1f1b/charset_normalizer-3.5.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8efc3f1563ed431882dd0dc0411b5f8ace1b1b89074981deaf6bd8af77dbe1bc", size = 193907, upload-time = "2026-08-12T14:32:50.414Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1d/cfe7b745ef7f4c3b7214581955b5a0869ba2ac551a58fc11036281ae167c/charset_normalizer-3.5.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:368eb2fc9482158b3a3386e8f01fa61f479c968e9a19ceab8f0188b86b312991", size = 197135, upload-time = "2026-08-12T14:32:51.605Z" }, + { url = "https://files.pythonhosted.org/packages/28/55/30fafdcfca9ba616bc394240545e4cd52f4f66dea43ded81b7d2d5274fde/charset_normalizer-3.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:826a295a039178479a325be1ae60eded1f0b10f7dda749df59e2440de8f61d64", size = 339892, upload-time = "2026-08-12T14:32:52.82Z" }, + { url = "https://files.pythonhosted.org/packages/f4/08/bdca5fc2bdc36ee443673dc7d12b23885a5a7b282bef85a1a4c3b325b40e/charset_normalizer-3.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70ff1c16eb0eb5ee6bb12739292347f981a5ba764cc4df1bc2e69b0405d4ac3b", size = 239439, upload-time = "2026-08-12T14:32:54.058Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9e/506c8d7a7722bba7c8cdd78c1b5ef23bda92bfbe0b3e28ea84673d519a0f/charset_normalizer-3.5.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3cfdab178a4add5483e26a9bb1c16d8018ccf39b4be7a3aea6c3979e6828f2ee", size = 227896, upload-time = "2026-08-12T14:32:55.326Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1a/dd828f2b1d6f4bf10821b9a74d866be05ffcdbfcddfc501d6fe6428762a7/charset_normalizer-3.5.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6083d10a846218502d664375b9448508d9fa580bd834567423156c6abfbe899d", size = 262548, upload-time = "2026-08-12T14:32:56.484Z" }, + { url = "https://files.pythonhosted.org/packages/be/81/196d26f6bd78b93e0d451b69082a71027ceeddd4b0be9170b81bb038f824/charset_normalizer-3.5.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0f211c21aa316cb6e2662e54a1194633a79d98a50a876addacfce7ba5b34b09f", size = 259986, upload-time = "2026-08-12T14:32:57.661Z" }, + { url = "https://files.pythonhosted.org/packages/3a/6a/5b964a1eb0f9075ecd45083eeb21aaec215334f98bac3d400302ea73875d/charset_normalizer-3.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b08ebf9488c7ff5eff038e48e6ea938178dfd9dcc8598b5ca941e4ae27b20be", size = 249853, upload-time = "2026-08-12T14:32:59.123Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b4/aee3a9d82edd0e931091ef3e9f03e46491ae3590e96e998d0975dadbe17c/charset_normalizer-3.5.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95450fce59f00c6d08eff6572ec2e736e5054c9450253afd5748f8416f2eb9", size = 244217, upload-time = "2026-08-12T14:33:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/22/3e/33f72ca11c1b619b220fd9f35905ebd171cbd0e0470f2357e467b9e861ee/charset_normalizer-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5780a29823e1d2bec69b7a104ead4195a43f3e97782efaedbf1f79a0157af715", size = 241307, upload-time = "2026-08-12T14:33:01.589Z" }, + { url = "https://files.pythonhosted.org/packages/78/27/6029dccba958621c7f3a65136f87c5512d712aef9e890f09512cc171bd03/charset_normalizer-3.5.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:054420b5db984971d886e5e4e2c37c760ae6682aedbd066687ff0949d9ed5f08", size = 233305, upload-time = "2026-08-12T14:33:02.866Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/691c967be459153fe9faf49bf78bc95639ef8bf6dd008f38cc6389a349eb/charset_normalizer-3.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d016dc857136c726958102c3b8a3986acdc65ace6fbf12cfdc09cc4bfa2935b2", size = 263465, upload-time = "2026-08-12T14:33:04.166Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2d/9202221be5c90b2a835924191e362690ed8dc8c7d6606100c2bd03fe0f8c/charset_normalizer-3.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f2ce3d39fb4a9d674e6639dd5d3146b2e273475d2260f10163228d66fc04433d", size = 245060, upload-time = "2026-08-12T14:33:05.325Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9a/298772fd0a0cbccadf36451a1cd7eef4b66a11e99b4a7f6fafc47cc62c75/charset_normalizer-3.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a5613a3a82c974227bde18f03409e30c467f8065cb56d822e3eb83708a5f223d", size = 261091, upload-time = "2026-08-12T14:33:06.475Z" }, + { url = "https://files.pythonhosted.org/packages/82/3b/1a11fe66e555dbe2f5714ade6ba74fa29edc9155d9cf1001d4d6ed096aa7/charset_normalizer-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fded2e82ff082e5d8e017e2ddcc1411bd8cb83b8585097fc401ef574f756b888", size = 251857, upload-time = "2026-08-12T14:33:07.784Z" }, + { url = "https://files.pythonhosted.org/packages/17/fc/73b817e8af3f1d25ec5cf458d405abba5a144cf9812238a61530f5eac186/charset_normalizer-3.5.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:8f006866047c6ec4b627ec144b1e0bbc7427cb31fd7c08d19897d0ac9032af3d", size = 139745, upload-time = "2026-08-12T14:33:09.123Z" }, + { url = "https://files.pythonhosted.org/packages/b3/fb/ddb66303c86f7dc5043a457dad9fa82b4d6d0cb97094f9bcdde21693fd58/charset_normalizer-3.5.0-cp313-cp313-win32.whl", hash = "sha256:196e270c4e80827b5072eed7d6aa661d133afada94fe366669f9609e718d305e", size = 177217, upload-time = "2026-08-12T14:33:10.305Z" }, + { url = "https://files.pythonhosted.org/packages/fb/88/6018cc8d76ea2b7cb02918f37e23e86c261d1a102713d7e88d2cfb8b211c/charset_normalizer-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:72982d9958a42f8132bf2d6b90214ed66477295ef1188731f98ae3511c6eeb5a", size = 198896, upload-time = "2026-08-12T14:33:11.51Z" }, + { url = "https://files.pythonhosted.org/packages/20/2e/04c0bbfc8d9abf91959f7a3d207d45cbf63a8116984caae2381890019bb5/charset_normalizer-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:0b373bab0b867b68b8eb249da9478cab9181a42993437cd2f5dba5fb0b4fbd1b", size = 179193, upload-time = "2026-08-12T14:33:12.726Z" }, + { url = "https://files.pythonhosted.org/packages/43/14/d098868dac5ff27e0258f548b1c74c6484be528384965d8fcf8fc6a4011d/charset_normalizer-3.5.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:d95244906ed69d0f79f190893c65e336c15959003e21449256dc05c001b52ea2", size = 211664, upload-time = "2026-08-12T14:33:14.153Z" }, + { url = "https://files.pythonhosted.org/packages/e7/da/a944b32a46601ae5a4c3499e8d64ecd14fe82313f00da74dcdf00273a0b4/charset_normalizer-3.5.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:d788e2ded0c4c47efa4d73cfe59eaf975ee32f425219873d2cb3e3fbaa00f636", size = 224375, upload-time = "2026-08-12T14:33:15.472Z" }, + { url = "https://files.pythonhosted.org/packages/f7/db/eabb5996be2f529744755e7b2fc9396eff4a64961f034e7fd49d54b9afb2/charset_normalizer-3.5.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:f9f91d3e8382900f3a68fa0ce94294479de9cd2de6bc0c70acd0f0dfd511836b", size = 194364, upload-time = "2026-08-12T14:33:16.607Z" }, + { url = "https://files.pythonhosted.org/packages/78/65/4ad3c5be108930310d8003f5602861d5b89f728293b9f09c3a4837f7ba10/charset_normalizer-3.5.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d54625cbf4e6b60bf0639728cb8b4cb541e340f6d7cafae5806051a40ddf4c45", size = 197643, upload-time = "2026-08-12T14:33:17.88Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8fee3201b98d52289be60a775797d69be05a04fb6cfb48c1587dad33e649/charset_normalizer-3.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:1f99a8c3a1da5d955edbad18208b3d627bdd54c48a6e739fa877bdca98c686d6", size = 341384, upload-time = "2026-08-12T14:33:19.239Z" }, + { url = "https://files.pythonhosted.org/packages/a7/dd/9e757101d1f76c35c0643684ba499ac3a181fb2b264c68174bf727d627e8/charset_normalizer-3.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf1e75dc07a3850b53d1e5f75e04d3ae12afe56284be7821771eaa2466350c73", size = 241637, upload-time = "2026-08-12T14:33:20.619Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/7857023015400bc4aa0a82fbcca29fa2dc7ec25f971a130764cb2dc7a589/charset_normalizer-3.5.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ac5a9cc079c67d75f4ddf343276031879eadbb333d1bb231cce297b8d7b9aae8", size = 226170, upload-time = "2026-08-12T14:33:21.773Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ae/8b52935b304f7b6bbf33151ed2b75266b09aa4b6f8f04230d948885b2577/charset_normalizer-3.5.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0dfe83c1b4d00abbf433998117a14f56a5c2bc68226c0d331709eed0d1ce539b", size = 265093, upload-time = "2026-08-12T14:33:22.999Z" }, + { url = "https://files.pythonhosted.org/packages/dc/78/6e838f6bb059f2c0afc60a4e7f294252f043c254656ad4114c50302cae4d/charset_normalizer-3.5.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e4e8fa586df2208ef040684751345f10f503834a757c9a74ecd19c1a2f9b1ccd", size = 262789, upload-time = "2026-08-12T14:33:24.214Z" }, + { url = "https://files.pythonhosted.org/packages/c2/08/189b27e51fddc9d6b3695331da0e31792c1d88b953ad854e57f06e9b2cc8/charset_normalizer-3.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82cc5835997ec78afe293a192e385099355770a7db94b2fb1239d36b32796f1c", size = 250580, upload-time = "2026-08-12T14:33:25.707Z" }, + { url = "https://files.pythonhosted.org/packages/ac/55/64854e99b25841f83e8e37d9df2f3d1f96f693439f80e5fabd542a7e47ab/charset_normalizer-3.5.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:19e52bda45086df8a4be4bb5910af6f5d9d3b538c78712c8ae09ef10b85bf458", size = 245008, upload-time = "2026-08-12T14:33:26.971Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/7720c904fa635d4260b4dced6029cf3d298c57b26741365d5a8d28c54043/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3418edd0ecb72a0a3861cf72f31be0ad9b7fe338ce2b58fb5cc80b9aeb792700", size = 243892, upload-time = "2026-08-12T14:33:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/70/50/7bfcb327631d4870c720872b548745f6ec8baa044d51c21b5d1d32ac4e3a/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:125ee619611019471b177c70bc3e9d4cda9fad7e01d93523501d3b188df0193a", size = 230996, upload-time = "2026-08-12T14:33:29.511Z" }, + { url = "https://files.pythonhosted.org/packages/24/51/40c45d6d940c04005ed721aa54bdebf1ebb2930f8a2ae537e8d60484fb27/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a3ad0e3da22852533858663848608f3f24c0d35e5cde415a4903476f2b4c88ec", size = 265834, upload-time = "2026-08-12T14:33:30.689Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d4/ef7a227ef89d215b47f9df79c3966610b17faa13bb2f236989207a631622/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:4ebebb410bc517e1d284c52a123e82704b21e4e7e26a21ebecf7439d0647b8a3", size = 245544, upload-time = "2026-08-12T14:33:31.859Z" }, + { url = "https://files.pythonhosted.org/packages/37/a9/a4ca9156964ded61c7718eba410ce11be2fd2b263fda4bcf08367b6578cd/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:91f9f7c151e772acebe489eaec96e96a2877202d7dd144e3f96b8676881715a0", size = 264110, upload-time = "2026-08-12T14:33:33.13Z" }, + { url = "https://files.pythonhosted.org/packages/38/6a/838364bb8702229c6e5f8b23f80ff0f052a12dfaf3113a12fd6acbe92a44/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:401ea6e7af9e7852ed818f64714b579c1935482049670847ca3bd7ba45dc63fb", size = 252303, upload-time = "2026-08-12T14:33:34.98Z" }, + { url = "https://files.pythonhosted.org/packages/9c/5f/d88032edce951f499a2321cf7ae0d35a043c74be12bc22d81084cc7afbcc/charset_normalizer-3.5.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f7496aed56b06325a1ad419c5bf23c6dd042558e874f71dd1b958f3e255f3053", size = 139964, upload-time = "2026-08-12T14:33:36.195Z" }, + { url = "https://files.pythonhosted.org/packages/37/ae/1c4a46b6b00d1c34d2ee355ef99ad6173674166800d1af0f05f85028d513/charset_normalizer-3.5.0-cp314-cp314-win32.whl", hash = "sha256:606a86c1c3196f3738de39a67a7490bbd61cb31c0e0436070bd0c6a48170b38e", size = 179790, upload-time = "2026-08-12T14:33:37.356Z" }, + { url = "https://files.pythonhosted.org/packages/01/51/f94dcf34fa8eba48c1fb89b6490a5f1426e19488fe5f38aac6c648c99057/charset_normalizer-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:ec6c464cf45867f66a2273e2214d9199a8fbad5cb95ca0fd45f6a2fe1d9d2cf4", size = 203723, upload-time = "2026-08-12T14:33:38.639Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ba/91d386870b5d9e4b0d8c4034f63877cc2e47b99c81ef05f3e6d42bf9a53f/charset_normalizer-3.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:dc28949de1bb5f7f30a46f15d74ce7ac5aaa63e03c5de04d68f571c7423af834", size = 183423, upload-time = "2026-08-12T14:33:39.899Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c9/534ecb17b7fb95f9052c4a44cf316316a27d4a8f73e8475ff55e778dcdd7/charset_normalizer-3.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:68b7e84ae8239a94f8d2c8f3f3a3a81bcde54805ec8f42a34de927d155688ec6", size = 368967, upload-time = "2026-08-12T14:33:41.093Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/ad7c3242d7fe55cd55126c22c65cb1b49779782cdf8932fd01d12232d86a/charset_normalizer-3.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58ca5dc0a0ef99f2801ec0574214c978e9574055bc783830bbb6e7433218609f", size = 239478, upload-time = "2026-08-12T14:33:42.428Z" }, + { url = "https://files.pythonhosted.org/packages/7e/62/77f0b850048e430fc350ec58876b0c020f5c8d0d3956fd1a4d6ae2fa292f/charset_normalizer-3.5.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6c57af4084c10cb3286688d65e4c654190ff5edcbc2411d08cdca0a8a44c59a1", size = 227036, upload-time = "2026-08-12T14:33:43.635Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ba/47d951e1a51dddbaad0a1410baf49fb1d897ceb00281568f1183b79bce9a/charset_normalizer-3.5.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1619a3cc174a7e3963dd34348e6fceb6e50db0ddeb0031bd7c73a58286454fa", size = 260772, upload-time = "2026-08-12T14:33:44.96Z" }, + { url = "https://files.pythonhosted.org/packages/b0/61/8c7ff4c81b2a88271126acf4b83ab3e31f6d63868b0f01d331eaa0f9cb67/charset_normalizer-3.5.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1328cc57dd4372be1265f68232cee890e087416e3e6e93e6ffb32c2bad4d36a4", size = 259273, upload-time = "2026-08-12T14:33:46.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fd/36129689be08dc287b951306946657ff70d76e287dd57018861f86d0e474/charset_normalizer-3.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06f4fb62a9139bef056b8b2da6773c94c2f259f90e4b8e53b166f3d0372d7cf6", size = 248086, upload-time = "2026-08-12T14:33:47.54Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f8/bcae67f994c8fd31dda445e5ebf84045823c31443fe46f0e9ee6aca99aa0/charset_normalizer-3.5.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:478650a70a750d75d5add401606c77f77069c32e4ba2c9131dc6cee566962ca0", size = 242671, upload-time = "2026-08-12T14:33:48.746Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/0472cdad1061c2f0e4d3aee29973eb6e81bb8fe256ff2860cf115b15f1c9/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:48920bf6fe83eb2226756ac623fa54940487154eb18f80889d5735cf234965c0", size = 241311, upload-time = "2026-08-12T14:33:50.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/89/04a03de5d27c77c624d9fcf6287073754bd438df1b58cb7d030c57c2824d/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:168a0cb536b5123a77bc42ecf5e0bf6f923d0d9ae43c42a14eb0677c19ac6c19", size = 229898, upload-time = "2026-08-12T14:33:51.523Z" }, + { url = "https://files.pythonhosted.org/packages/42/a2/639c4278adcb7ed1f4db608dd9ac19b6774fa2285a96b1c0bdb9c124ccbd/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f278e131afa96a3622cef9211c406ea2ad1b68eb06f8837cd443684a40e0ae50", size = 262852, upload-time = "2026-08-12T14:33:52.924Z" }, + { url = "https://files.pythonhosted.org/packages/9d/95/02e34c97bedfd0c5574efb9179c850591acc7f967ba039ed8dd29d332b73/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d6100f877d2ed95f0856a3fde25334153add94bf2224c43f45f88e7039262aaa", size = 242913, upload-time = "2026-08-12T14:33:54.18Z" }, + { url = "https://files.pythonhosted.org/packages/a3/64/0946aeab6462dad9f160a50dfb4704d3f58a5ee708f085abc2105fbbff0c/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:4c440122e1ea68b1f8b44a631ebf49c39180f6869b1da22d76e8a724208ec6e9", size = 257938, upload-time = "2026-08-12T14:33:55.802Z" }, + { url = "https://files.pythonhosted.org/packages/79/77/36787d41ead124746506a4425c729f4f17c68280af8a6a5baa0a598cae86/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e5f834965c2fe589837bac1002e07e25734ff70381903ccd95b3d649e22bfa40", size = 249467, upload-time = "2026-08-12T14:33:57.081Z" }, + { url = "https://files.pythonhosted.org/packages/65/10/d9f6c5589cd24198d4ce6cd2948191c18e657272f433e5a00d258d9f5c22/charset_normalizer-3.5.0-cp314-cp314t-win32.whl", hash = "sha256:076cf9d3f3c7e410295c09d96355cf3b1bcae74990034d80e4371e20fe1ba4c6", size = 190624, upload-time = "2026-08-12T14:33:58.449Z" }, + { url = "https://files.pythonhosted.org/packages/6c/81/43e0584a802051a22c725795ebe1df78263abc7de858eef6cdc9b36637e9/charset_normalizer-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3288a560dc3114d5d2ebe309b1ef43f8af355eafe25856832415c2a8196c9db3", size = 215902, upload-time = "2026-08-12T14:33:59.753Z" }, + { url = "https://files.pythonhosted.org/packages/30/f3/af6a1160fef0eac4510d035241e11eccf78e5350e4cd4de79e79fe02a5e5/charset_normalizer-3.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a284c36b9c6616bf0a8aa4aabba668a0c75ba65ccf40a79868aeaa69ad996897", size = 193452, upload-time = "2026-08-12T14:34:01.017Z" }, + { url = "https://files.pythonhosted.org/packages/42/a4/dee470afb7a55c4f78b6fef37306c51fed17ebf94dbe530798c91d394350/charset_normalizer-3.5.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:c38d1e9bc2073b0984d2099ea647fd7f6c0d8f83a1e14e0cd32926f16e4c44ce", size = 341595, upload-time = "2026-08-12T14:34:02.4Z" }, + { url = "https://files.pythonhosted.org/packages/6a/32/9c3126dc429c6d9d7f79c52681a7c4453ed20a26267c9a8275d7ab620aba/charset_normalizer-3.5.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9f45186390aee4d1f26f723c615b67df346766c3b16df000d84d6e374f06757", size = 242177, upload-time = "2026-08-12T14:34:03.741Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9d/5b616a887301ff4cc0916b39ba44257390d3da80deeed6e8b6f2f26b14a8/charset_normalizer-3.5.0-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f0fde5e5100c735b2274ab898f0742a5dcde492796296cfbe7e0ad6a4cd1a396", size = 236730, upload-time = "2026-08-12T14:34:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/d6d3e70be93ebe5fabef65e4c7ac113e1d1705cbaeb5fb72467e713aca17/charset_normalizer-3.5.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d00e18e7bbf47e332ab63903d18bae31efc701b1d8cca0382b97784a621fc44", size = 265158, upload-time = "2026-08-12T14:34:06.235Z" }, + { url = "https://files.pythonhosted.org/packages/30/e7/3f1fafa87e2643257474f9c4eec609f2193a61d907dce7dd4f3f2390ebd5/charset_normalizer-3.5.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b81980668800dd1c69faad8aea6e85a8cee0e13bcd3bba7671695ff16260293", size = 262931, upload-time = "2026-08-12T14:34:07.511Z" }, + { url = "https://files.pythonhosted.org/packages/0a/df/ebeb224a949d91829e5e114c6b64372a3c792b00762a9e951ce416f3a32d/charset_normalizer-3.5.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb3e0d1345b9c0fe73673ea656375f38a78ec679c2edeae0c24800f04798a85", size = 251388, upload-time = "2026-08-12T14:34:08.949Z" }, + { url = "https://files.pythonhosted.org/packages/a0/64/9a6ce2e7acc5cf1b4636f78f82e89ff581e06a0216a40678b28bd4d832c4/charset_normalizer-3.5.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2401f7671242e921e604f609d429f6b282ea4ca787a6ffd22ed7372011ddb9d1", size = 251821, upload-time = "2026-08-12T14:34:10.138Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b1/6e69b8056f615e5ccff6b91ca16db2d47922251f016821a300c115267fef/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:96720f2aeed3434bc48f4d52fbad64ecc820cfed88915d664780ed9ba09ede78", size = 244507, upload-time = "2026-08-12T14:34:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/0f/34/02c15d6a0aa6b934dcdc136b111da63ae857b9fd51cf5505b0736337c2eb/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:c455829625df983f716cbaecbba77f2d1dc2e0e0ed1638c059cece15a279344b", size = 240951, upload-time = "2026-08-12T14:34:12.991Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/0fe893d3e1c7d111280bd6c4bd4c1e431487a1124a1bcbce78dfeda3a3a8/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:c41b067eddcfa5ee6b1169c287605be7fb6b0ea22bba6474c5bb978a668def4f", size = 266162, upload-time = "2026-08-12T14:34:14.232Z" }, + { url = "https://files.pythonhosted.org/packages/55/ea/eca03527307670f5d102c295671a800c404ca958cf94fefd10fc963a72f0/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:e31786a947b136329bfdc458c82c06d4ec539b4a4436b7da4df4aafc9902ee80", size = 251835, upload-time = "2026-08-12T14:34:15.48Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/fee5633081e595fe9e191df6f215106c791ad596eddf5e41e39b8ea0f2e2/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:c5c6d47a865147e0ae3322ce92e7fb52ba3169d94b447deda56897ea2aa6fac9", size = 264314, upload-time = "2026-08-12T14:34:16.679Z" }, + { url = "https://files.pythonhosted.org/packages/cf/fb/17f47ae6ca35b562fb6e6f4b05f7aec6034217353eb4a23aaa3566dc7340/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:c75191e3c8052045179646cb40e280800a4e0bdfda34d9c949c2f268d44e80e4", size = 253194, upload-time = "2026-08-12T14:34:17.975Z" }, + { url = "https://files.pythonhosted.org/packages/59/88/f2b0f7ebb92493e925889ff29239b3b0073ffafd91230dbfc69e5cf9389c/charset_normalizer-3.5.0-cp315-cp315-win32.whl", hash = "sha256:83b62410bd36bb1178a7d563e2ee0cf21eb1c980c912ab99c2c78f06227f1731", size = 179800, upload-time = "2026-08-12T14:34:19.409Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f0/afb5bfdea52fd943b1960403847a276b8e900c6e4cd6a38752321b4eda64/charset_normalizer-3.5.0-cp315-cp315-win_amd64.whl", hash = "sha256:e3b9eaa99a6d8c9ace4cd303915947ef55088d4cd87c6676874f98c5c03aa040", size = 203726, upload-time = "2026-08-12T14:34:20.656Z" }, + { url = "https://files.pythonhosted.org/packages/fc/71/219783eb691aa2ec879c0e521afdfe2b826f9678eed51b9c039d03e0db2b/charset_normalizer-3.5.0-cp315-cp315-win_arm64.whl", hash = "sha256:fec352b793cdc183cc9e7e0b6c10fd7bff38ec54ba44cc43599b9b56f7f3db2e", size = 183428, upload-time = "2026-08-12T14:34:21.975Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d0/14aef3b9f80f2593c039d897e89034635b9eb0eb44b6ce5173bbd79ff338/charset_normalizer-3.5.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:c9bde7a960720c8b8e1b5ef7afaa0c9a2f3b55c44abd635b2b29dd066b298e3a", size = 368728, upload-time = "2026-08-12T14:34:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/10/fc/b249466ddbbeffa448b6597631e9091d1f01b5132ff8e7a0e21a6eb72b63/charset_normalizer-3.5.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8cd1283a9fe6c2065c807e9d5da81afe5e1e004caef39adc0d8ae86dd883698", size = 240925, upload-time = "2026-08-12T14:34:24.504Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b9/c17e72aaa1b3e1ca6c184e8025cf138ed492d01a54f85286ff7d31253a4b/charset_normalizer-3.5.0-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1c010dd86d3f4c4433c9634d33ce8147393b270dfa54f217f965540b8ae8e075", size = 234932, upload-time = "2026-08-12T14:34:25.822Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/f84ef0966bbe216f71029e34e7fa425a16b1682e2a40265e679dedf2b655/charset_normalizer-3.5.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5e68229977b2dea28e7061c0c0630a23f2f9f6e9c6fb38d77d3d6dbfe3768b74", size = 261733, upload-time = "2026-08-12T14:34:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/3b/73/3e887fa0781a395339355ed934ab6561ceb5bb52574160f070224039c630/charset_normalizer-3.5.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a60773eb5fda796e6e6f76b9c152d270fe59f9788a51a6ff8ba44082d8548ae4", size = 258460, upload-time = "2026-08-12T14:34:28.431Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/52ebd9849bf9e35d0b21fff115cb6543162a8e1f2f564e8f87121a336b8c/charset_normalizer-3.5.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9419f44e568f7fafcdc0b3b5c766a2364e705a9b34fb8a56b431e0d1f3f4258", size = 249894, upload-time = "2026-08-12T14:34:29.698Z" }, + { url = "https://files.pythonhosted.org/packages/85/f3/9366492b8a5fe0187de282e001d61345740cf79eb4a5f20181d769be02b5/charset_normalizer-3.5.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75e243abbb528c1a774390ed71e3f868a9f37b1373442e4bbadd401cfc505ff4", size = 249540, upload-time = "2026-08-12T14:34:30.96Z" }, + { url = "https://files.pythonhosted.org/packages/0b/af/28bb5e5dbd3e67cb9196a62781ac2b6d79492f4fc7a069b6ca7d6d6c8d58/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:54c963ce6404e52255b737e8a06d356fc762d59096ae566203a67cf2b7d050f2", size = 242734, upload-time = "2026-08-12T14:34:32.481Z" }, + { url = "https://files.pythonhosted.org/packages/26/d6/7ccfa62b53b40fc06b2d3504825aa400764740bd10cf248fdc4272441b93/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:63ea0cc840c66670183578c2630d138c0e944aeadfc33f25173ee240f5db780d", size = 239580, upload-time = "2026-08-12T14:34:33.916Z" }, + { url = "https://files.pythonhosted.org/packages/0b/82/71c0c9b046697b8da66b3acefa8d5f92d00a9ef433ad7c3522b971d0369a/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6c06875a1d4a7537bef70f659b55c6b55b9a47ec3ba8f2db610350c2d9915e6e", size = 263281, upload-time = "2026-08-12T14:34:35.333Z" }, + { url = "https://files.pythonhosted.org/packages/d6/01/d027583c869f40ba980c1c76994adbd522c360a6327e72beb44d7c267385/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:4253da1b4456b633651a8d59eb1dc7a8a8fa38241014dd7c217b353e547ae394", size = 250027, upload-time = "2026-08-12T14:34:36.991Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e9/6475d739e0ec8bb1236e06263dc3affaffdf947d8114ad27024932f325da/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:f044cb1cf44012184715f46584658993b5fee9344d71c4b0c455a17a299730c0", size = 257547, upload-time = "2026-08-12T14:34:38.277Z" }, + { url = "https://files.pythonhosted.org/packages/a5/60/d1f502fcaa048a2aca3ab80bfef8407659c131e4f1792fa805fec14b4960/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a17864853f7c518ae7d4b368af98f427f9396805476af40af8698560f09d7d97", size = 251718, upload-time = "2026-08-12T14:34:39.562Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/76343dcf4381a698807ff8a20d89f66bbdd9f6222b0b17740f77ab764335/charset_normalizer-3.5.0-cp315-cp315t-win32.whl", hash = "sha256:9e726478d7a213847860219d74665a6892a643ac93b8f76580f6cf9ed39996b7", size = 190757, upload-time = "2026-08-12T14:34:41.053Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ea/d18147626a1667cc773c42104ab155a4ca5d6d4d174b7a35e01062213ea5/charset_normalizer-3.5.0-cp315-cp315t-win_amd64.whl", hash = "sha256:d7229a99120c6c2792d96f4857c2648ce5530e93667a2c2388c5ef69a6b84775", size = 215431, upload-time = "2026-08-12T14:34:42.501Z" }, + { url = "https://files.pythonhosted.org/packages/47/21/4869598aae0872d94faa5933918a4fe37ab2c5af9d095786e241f9506fed/charset_normalizer-3.5.0-cp315-cp315t-win_arm64.whl", hash = "sha256:527e28a5e751d9e11369b9c5f9ab35c748eb9c109101920c7deb40d6eadf8d03", size = 193205, upload-time = "2026-08-12T14:34:43.781Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f3/7b523d807cb5e73562ef8acf21d39cdb9d704955327362c781bc3478a73d/charset_normalizer-3.5.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:5a4ee37248dfac25107c758bda99d545ce73e60b44d2dd39e4a2bb9f2831e9f5", size = 330840, upload-time = "2026-08-12T14:34:45.06Z" }, + { url = "https://files.pythonhosted.org/packages/f0/de/fc68978fe78ca97063c96d764e41ff92ca639948f319271e0ff450e577a2/charset_normalizer-3.5.0-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a864bdcacd8bff58bb4845304e031f821a3ec64b2b7259f2d409cd49c9e59ca3", size = 251862, upload-time = "2026-08-12T14:34:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/a9/cb/82b41a0ab7fb1a88065f1d78ad32696ad88ea3fe8e25b8189d08833938de/charset_normalizer-3.5.0-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84b736e3b391601bc47b86da381c749c0f894e9191aaca9f31f30c2632206df3", size = 239484, upload-time = "2026-08-12T14:34:47.869Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0c/19608b631f4538f908098d4a2d56a8f79a665e27cc58e9d90479761a9227/charset_normalizer-3.5.0-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6abb1f356fb865baeb6ebc3fadd843e9a96fbf49b9adcca55037f3cceccb7438", size = 230602, upload-time = "2026-08-12T14:34:49.265Z" }, + { url = "https://files.pythonhosted.org/packages/29/db/f648eb30e14eba301aed61e11672156f137905c1bdbb530151abe8065943/charset_normalizer-3.5.0-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d366548d2ee28a8cfdcc4296363978cc644a728333be9824d2de4652e83df0a", size = 259208, upload-time = "2026-08-12T14:34:50.632Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e0/ed2c8bdbac484d69614d6993143aeb6cb0f4dd1561c883402517b623c8ef/charset_normalizer-3.5.0-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d672f329ae504ee240eb39b6effb3318aa8e7e8924c0ce8eee5760b3fad98539", size = 253659, upload-time = "2026-08-12T14:34:52.11Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/b4907cb9ec5b521d9d024ced13611240b86ef065c2eb15b3ad2334dc9940/charset_normalizer-3.5.0-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c54036a518748b6c02e666f6d46c3817561998fb904c3be25b56fb4fe3dc5706", size = 248821, upload-time = "2026-08-12T14:34:53.399Z" }, + { url = "https://files.pythonhosted.org/packages/12/b2/e2d1abcfbc05822f0030869efb4e9f8a3658e13b4821796d4b62da917327/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:22a1889f1c9b752c63c36758a0c2145458e3cadb20fced7a0790002e9dd12b26", size = 240271, upload-time = "2026-08-12T14:34:55.09Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f9/4ba127ad610542fa3eabfa41c45bf12d357860a815b3566374ec0188e213/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:b7eb3eab5c646d3de7dcb14a7c9caebace5249c5767da39e1761cb1576e521a3", size = 232155, upload-time = "2026-08-12T14:34:56.543Z" }, + { url = "https://files.pythonhosted.org/packages/01/68/40613182366d00bd6dbd5f6c84a926cbd120960e038a8269e9ae7d782762/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:2080aa129a28267984cdc902898993d788c995c384e285d0d19199f56760d52e", size = 259674, upload-time = "2026-08-12T14:34:57.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/53/4574a14fa4c9de4a6c9f31725354bfa40b67f653e6d594ce1654f9a41b32/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:fd68c825548a611158230e2f9222e210ceb2e3391995c0aa5865cbdf3ab4bd49", size = 246122, upload-time = "2026-08-12T14:34:59.337Z" }, + { url = "https://files.pythonhosted.org/packages/5a/02/bd8030d13d92c058ca7b2b9615bbb3169569e144db64d65c149cd45abf5e/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:d8a9316f4da85e937242642b537c6d55d7e9287dd38e5634732f8233932aff45", size = 255221, upload-time = "2026-08-12T14:35:00.71Z" }, + { url = "https://files.pythonhosted.org/packages/77/9d/10ecd3bcbe2666b3d4d4026c97b48f73990682815db516052a1e8f4a31c5/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d90254c8f609338c53ec180fcd4c4f9c16502e238e3fc88ca7fd4c2f38d445b8", size = 253450, upload-time = "2026-08-12T14:35:02.217Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/d044c4872c0938a84f87b5027a698c0e61bacfc5c3551a4e749ca9b7bc5c/charset_normalizer-3.5.0-cp37-abi3-win32.whl", hash = "sha256:8b3e9e29b8b07cc461b9ce7768db7693a93979d0dadf22046f6f3555ded2f516", size = 173594, upload-time = "2026-08-12T14:35:03.845Z" }, + { url = "https://files.pythonhosted.org/packages/10/6b/6046773901f1944b9a89436351529811ee958afc7b774563be9d74a6f0c3/charset_normalizer-3.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:0c8953d9d1617794cfc40d81179571c9ba3805dd029623a15c93f1fb70e60a74", size = 198959, upload-time = "2026-08-12T14:35:05.187Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a6/b57708ac92aefc8e8389d51d5178129b81f03196da61ee2c23e687b8178a/charset_normalizer-3.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:562d24ca7797c1af8852994950c2e623a907b201fc4b0ed29e92af173d3828ca", size = 267055, upload-time = "2026-08-12T14:35:06.533Z" }, + { url = "https://files.pythonhosted.org/packages/22/c7/754d09943a616937df61e4ba367c409ded2a987e872972098d51a6fcf73b/charset_normalizer-3.5.0-py3-none-any.whl", hash = "sha256:993dfcbe75a85a3784abb5084f2c41b915767c90546fcc92803cffa28611baea", size = 67943, upload-time = "2026-08-12T14:35:30.363Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cpex" +version = "0.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastapi" }, + { name = "httpx", extra = ["http2"] }, + { name = "inquirer" }, + { name = "jinja2" }, + { name = "mcp" }, + { name = "orjson" }, + { name = "packaging" }, + { name = "prometheus-client" }, + { name = "prometheus-fastapi-instrumentator" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pygithub" }, + { name = "pyyaml" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/ed/85fea0d1a91a4b10fe2bc00ea61879ebf4b87c5e17a16c3fad6f9dddac8e/cpex-0.1.3.tar.gz", hash = "sha256:a9e2bccc82e8f8ac54839a9aef434193e943e0c6eca2d5464f3011ef59f7302f", size = 2390201, upload-time = "2026-08-06T20:02:23.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/b6/406951f08b00aff2dd24d54fab16413ca4a72906305fec61245fdfc0cbf7/cpex-0.1.3-py3-none-any.whl", hash = "sha256:a5d33bcd1b4328b36e9798a1258e9d6465e073c784e5b9d1c4588f1b93054848", size = 279062, upload-time = "2026-08-06T20:02:21.814Z" }, +] + +[[package]] +name = "cpex-output-length-guard" +source = { editable = "." } +dependencies = [ + { name = "cpex" }, + { name = "mcp" }, +] + +[package.dev-dependencies] +dev = [ + { name = "maturin" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, +] + +[package.metadata] +requires-dist = [ + { name = "cpex", specifier = ">=0.1.3,<0.2" }, + { name = "mcp", specifier = "<2" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "maturin", specifier = ">=1.13.3" }, + { name = "pytest", specifier = ">=9.1.1" }, + { name = "pytest-asyncio", specifier = ">=1.3.0" }, +] + +[[package]] +name = "cryptography" +version = "50.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, + { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, + { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, + { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" }, + { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" }, + { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" }, + { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" }, + { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" }, + { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" }, + { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, + { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, + { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, + { url = "https://files.pythonhosted.org/packages/9d/3e/e54cde8c01631a5a8226ccd617eab9e57fd5cfdad90f1a9e6bb570794631/cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c", size = 3963170, upload-time = "2026-07-31T14:24:51.968Z" }, + { url = "https://files.pythonhosted.org/packages/01/b6/0b9e125e90f3d2dcf599a218a899cda7326a3158cfa258723f0b398b08f6/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a", size = 4692441, upload-time = "2026-07-31T14:24:53.743Z" }, + { url = "https://files.pythonhosted.org/packages/53/c9/a5151588710785a96d7bc4de27d4cd62f263bbbcb203cfe29df537eb6505/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e", size = 4699810, upload-time = "2026-07-31T14:24:55.746Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1a/15b92b25eb6ce3089cd49377ae990a0f3ad485a510f968aed1f19dbdcdf2/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d", size = 4691924, upload-time = "2026-07-31T14:24:58.082Z" }, + { url = "https://files.pythonhosted.org/packages/62/15/219075012ab13e8905f3cd572204f4acb4b111df787104346b9bc0cea789/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437", size = 4699593, upload-time = "2026-07-31T14:24:59.951Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b5/c2c5fce26f0ee40d21bafe7f191d29a34b35a65ac4fe8a1191d1983612e9/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9", size = 3813796, upload-time = "2026-07-31T14:25:02.298Z" }, +] + +[[package]] +name = "editor" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "runs" }, + { name = "xmod" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/5f/fe06c2a13a5282dcef4c7133bb348d4125a9aa69c5fb49037a004599d73a/editor-1.8.0.tar.gz", hash = "sha256:b07e1bbcb8b33f05c2e6ed3ce77ee9756354ada840a18aad7c0536d967fe4c0b", size = 27455, upload-time = "2026-05-09T13:42:59.796Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/b8/6648cadf38b61045262bf2a534e526f04eb833275ad1fc77a2026e9f7e3a/editor-1.8.0-py3-none-any.whl", hash = "sha256:7d47ff88ae6c5f6c43d28c30b6f7fd59a24741175a1771ab06c969d946d7dfd0", size = 4012, upload-time = "2026-05-09T13:43:00.847Z" }, +] + +[[package]] +name = "fastapi" +version = "0.141.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "h2" +version = "4.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/85/7c366e69d84c17bb778fe41419e1fbcce3033d5b7ce29bbffff0a98b859f/h2-4.4.1.tar.gz", hash = "sha256:4e866ffb1a869ae14dd9b5e6beb5c24a13da0495ad72b65925ded182521c1516", size = 2157281, upload-time = "2026-08-03T11:45:09.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/22/e85faf23bd72a92d1921e37d674ca56eb298a3c8be31fdecef0ff2b3aaac/h2-4.4.1-py3-none-any.whl", hash = "sha256:0e25f1462b23c9cb82d9eb02e28bc706dac2a68cb457c6a0d74d63c8a2a5d0e6", size = 62636, upload-time = "2026-08-03T11:44:59.164Z" }, +] + +[[package]] +name = "hpack" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/5b/fcabf6028144a8723726318b07a32c2f3314acdff6265743cf08a344b18e/hpack-4.2.0.tar.gz", hash = "sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0", size = 51300, upload-time = "2026-06-23T18:34:46.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/b4/4a9fcfb2aef6ba44d9073ecd301443aa00b3dac95de5619f2a7de7ec8a91/hpack-4.2.0-py3-none-any.whl", hash = "sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986", size = 34246, upload-time = "2026-06-23T18:34:45.472Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[package.optional-dependencies] +http2 = [ + { name = "h2" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "inquirer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blessed" }, + { name = "editor" }, + { name = "readchar" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/79/165579fdcd3c2439503732ae76394bf77f5542f3dd18135b60e808e4813c/inquirer-3.4.1.tar.gz", hash = "sha256:60d169fddffe297e2f8ad54ab33698249ccfc3fc377dafb1e5cf01a0efb9cbe5", size = 14069, upload-time = "2025-08-02T18:36:27.901Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/fd/7c404169a3e04a908df0644893a331f253a7f221961f2b6c0cf44430ae5a/inquirer-3.4.1-py3-none-any.whl", hash = "sha256:717bf146d547b595d2495e7285fd55545cff85e5ce01decc7487d2ec6a605412", size = 18152, upload-time = "2025-08-02T18:36:26.753Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jinxed" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ansicon", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/d7/6e6d474ec5eaeca6a61acc17766bb19563b3a372b4b9d92910078f5fe49f/jinxed-2.1.0.tar.gz", hash = "sha256:7e755b831faa2443d44fb4ce7c0202eb9c3ed39bd5bf1193365888f4f6092b54", size = 61287, upload-time = "2026-07-03T15:46:48.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/1d/0a6be99b69bb448448403a45a241aff0dca751832ae54f027ed940d2eb52/jinxed-2.1.0-py2.py3-none-any.whl", hash = "sha256:43b802d18b70e405d410fb66eb2837d1101e7e5ea922e666507bb43f34d11d09", size = 104999, upload-time = "2026-07-03T15:46:47.114Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "maturin" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/b3/addd877f871fb1860d46d3a4f206ecb10b946c85846805e6367631926fd3/maturin-1.14.1.tar.gz", hash = "sha256:9d6577a62cd08e0ceba7a0db06fb098e0c9b1b3429bad747a4f3a18215a1b3df", size = 369637, upload-time = "2026-06-19T05:19:49.774Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/f0/97c5a5bd9c71653a066c0976a484eaaae50b9369557838a4176b7b0bdaa5/maturin-1.14.1-py3-none-linux_armv6l.whl", hash = "sha256:522292398945442cdafa9daeb2271b2340fbde57027b818f923f88eab04174f8", size = 10207496, upload-time = "2026-06-19T05:19:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/fe/83/294bca639b0e052f1e2f65199b3db258780c7d4e31408b934c9c974a1379/maturin-1.14.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ffe5ad71f21d1e6603c4dd75f7fee34adf5ed5ebcebb692886549888ebb329ed", size = 19680113, upload-time = "2026-06-19T05:19:13.43Z" }, + { url = "https://files.pythonhosted.org/packages/43/b6/79c881410a3b1c187f7eb3d407aecae646c6a4433d630d72200359015e83/maturin-1.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f3306078070c1508fd715b9116070cbcaff5959024272a9f1e6f5cb29768b86c", size = 10169205, upload-time = "2026-06-19T05:19:16.615Z" }, + { url = "https://files.pythonhosted.org/packages/93/9d/44b6f26dcb7f7a04c5501ac2dbb6ca1490150682baa525ca5860504f9eab/maturin-1.14.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:cd457cd88961156e26379e1155bd287cc0ec1c8b2f1582b0660fb31b87c8842d", size = 10188098, upload-time = "2026-06-19T05:19:19.736Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bd/9c0d5d6983905ce2c9edaa073a7e89355a9cf7f396988e05d32f1c37785d/maturin-1.14.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:dfc54ae32e6fcb18302193ab9a30b0b25eefffba994ae13238974805533ef75e", size = 10627576, upload-time = "2026-06-19T05:19:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/e5/33/b096412bd6a7cb399652b260666f901adf88a687181a6dbd6a3f89f0a94e/maturin-1.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a131d912b5267e640bc96d70f4914e10590aed64082ec9abacba7cea52004224", size = 10085181, upload-time = "2026-06-19T05:19:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/56/8d/08c3bf469c38a23c9e6c877e338193001eb604d010fedc08341974e38528/maturin-1.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:be18fc568fb76884c0205456336892a75105ec398e6b667cd777c6268bd06d69", size = 10026363, upload-time = "2026-06-19T05:19:28.904Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/c4d1a92839f8745ab4aab988a7db884a79d6d710bd3b286fcf9316dece1a/maturin-1.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:994a0c8ba3ad8a92b3a9ee1b02645d200d610216b15cff5102b0fe65e8e08666", size = 13321347, upload-time = "2026-06-19T05:19:32.411Z" }, + { url = "https://files.pythonhosted.org/packages/b3/fa/170f04624d03fd07d2a8b1b67de83a127af93aef9eaa425839553347297b/maturin-1.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be80866363e605d137991b491a741a84cde9ae350183c4c85f49690ca9aaaa65", size = 10877609, upload-time = "2026-06-19T05:19:35.448Z" }, + { url = "https://files.pythonhosted.org/packages/61/ad/1ae2e1d0ded282bf2c55ac13f0811d87deb425e200ae64a15785675dede9/maturin-1.14.1-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:5282dffd4b539d2be245f4e5b1a5ab6bc1033b58f4a4872f5833f9d43c954aa4", size = 10417316, upload-time = "2026-06-19T05:19:38.28Z" }, + { url = "https://files.pythonhosted.org/packages/fb/27/bf677183920718da49cd7982d6a3ffc440aad8919329f571d189f81b7bdf/maturin-1.14.1-py3-none-win32.whl", hash = "sha256:1a04de0a20188f95c721b5702eed18140bdcccb28c386797093eca3f62f4d4e0", size = 8931293, upload-time = "2026-06-19T05:19:41.183Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/585adeb9167b08d3cdff0032a938b0e72655c92003df4f52c3f696a1bcc2/maturin-1.14.1-py3-none-win_amd64.whl", hash = "sha256:3c9f94640ecc4895e94abaf834a0684430032c865b2748a36c12461fd9252fdd", size = 10314067, upload-time = "2026-06-19T05:19:44.389Z" }, + { url = "https://files.pythonhosted.org/packages/51/d4/dac8c0720ae246be1700afb6fbdbbea20fe35b13f6570b2f70faa005df77/maturin-1.14.1-py3-none-win_arm64.whl", hash = "sha256:15cea8fcb3ba47dd636f50092bb34baea8b04ac777392f23e6bf8a9a61efb894", size = 9718943, upload-time = "2026-06-19T05:19:47.49Z" }, +] + +[[package]] +name = "mcp" +version = "1.29.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/d3/f9acc21dfc886e4f78e2add1a47db46ce16884346afde53f8a064c02c891/mcp-1.29.0.tar.gz", hash = "sha256:52d01f334de1868cc3bb2d6604931126a67631f99a6c5d3b82ba47290315ec36", size = 643148, upload-time = "2026-07-28T13:41:41.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/c8/248b201f6d753d69fd5d6506011abbb35a946d9142b2ae311a948fd0be3d/mcp-1.29.0-py3-none-any.whl", hash = "sha256:f5a075bb611f23d6f4d080c6a1699fa62772eebc562ba9e66b306ddde1c755f7", size = 223436, upload-time = "2026-07-28T13:41:40.337Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +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" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/51/3fb9e65ae76ee97bd611869a503fa3fc0a6e81dd8b737cf3003f682df7ff/orjson-3.11.9-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f", size = 228522, upload-time = "2026-05-06T15:09:35.362Z" }, + { url = "https://files.pythonhosted.org/packages/16/fa/9d54b07cb3f3b0bfd57841478e42d7a0ece4a9f49f9907eecf5a45461687/orjson-3.11.9-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c", size = 128463, upload-time = "2026-05-06T15:09:37.063Z" }, + { url = "https://files.pythonhosted.org/packages/88/b1/6ceafc2eefd0a553e3be77ce6c49d107e772485d9568629376171c50e634/orjson-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5", size = 132306, upload-time = "2026-05-06T15:09:38.299Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/f11311285324a40aab1e3031385c50b635a7cd0734fdaf60c7e89a696f60/orjson-3.11.9-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c", size = 127988, upload-time = "2026-05-06T15:09:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/9e/85/0ef63bcf1337f44031ce9b91b1919563f62a37527b3ea4368bb15a22e5d7/orjson-3.11.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd", size = 135188, upload-time = "2026-05-06T15:09:40.957Z" }, + { url = "https://files.pythonhosted.org/packages/05/94/b0d27090ea8a2095db3c2bd1b1c96f96f19bbb494d7fef33130e846e613d/orjson-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62", size = 145937, upload-time = "2026-05-06T15:09:42.249Z" }, + { url = "https://files.pythonhosted.org/packages/09/eb/75d50c29c05b8054013e221e598820a365c8e64065312e75e202ed880709/orjson-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877", size = 132758, upload-time = "2026-05-06T15:09:43.945Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/360686f39348aa88827cb6fbf7dc606fd41c831a35235e1abf1db8e3a9e6/orjson-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1", size = 133971, upload-time = "2026-05-06T15:09:45.239Z" }, + { url = "https://files.pythonhosted.org/packages/0e/30/3178eb16f3221aeef068b6f1f1ebe05f656ea5c6dffe9f6c917329fe17a3/orjson-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd", size = 141685, upload-time = "2026-05-06T15:09:46.858Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f1/ff2f19ed0225f9680fafa42febca3570dd59444ebf190980738d376214c2/orjson-3.11.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff", size = 415167, upload-time = "2026-05-06T15:09:48.312Z" }, + { url = "https://files.pythonhosted.org/packages/9b/61/863bddf0da6e9e586765414debd54b4e58db05f560902b6d00658cb88636/orjson-3.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980", size = 147913, upload-time = "2026-05-06T15:09:49.733Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4081492586d75b073d60c5271a8d0f05a0955cabf1e34c8473f6fcd84235/orjson-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2", size = 136959, upload-time = "2026-05-06T15:09:51.311Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bd/70b6ab193594d7abb875320c0a7c8335e846f28968c432c31042409c3c8d/orjson-3.11.9-cp311-cp311-win32.whl", hash = "sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180", size = 131533, upload-time = "2026-05-06T15:09:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/3f/17/1a1a228183d62d1b77e2c30d210f47dd4768b310ebe1607c63e3c0e3a71e/orjson-3.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02", size = 127106, upload-time = "2026-05-06T15:09:54.204Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/285de5fa296d09681ee9c546cd4a8aeb773b701cf343dc125994f4d52953/orjson-3.11.9-cp311-cp311-win_arm64.whl", hash = "sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697", size = 126848, upload-time = "2026-05-06T15:09:55.551Z" }, + { url = "https://files.pythonhosted.org/packages/16/6d/11867a3ffa3a3608d84a4de51ef4dd0896d6b5cc9132fbe1daf593e677bc/orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49", size = 228515, upload-time = "2026-05-06T15:09:57.265Z" }, + { url = "https://files.pythonhosted.org/packages/24/75/05912954c8b288f34fcf5cd4b9b071cb4f6e77b9961e175e56ebb258089f/orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291", size = 128409, upload-time = "2026-05-06T15:09:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09", size = 132106, upload-time = "2026-05-06T15:10:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4", size = 127864, upload-time = "2026-05-06T15:10:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/31/6a/6cf69385a58208024fcb8c014e2141b8ce838aba6492b589f8acfff97fab/orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882", size = 135213, upload-time = "2026-05-06T15:10:03.515Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff", size = 145994, upload-time = "2026-05-06T15:10:05.083Z" }, + { url = "https://files.pythonhosted.org/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe", size = 132744, upload-time = "2026-05-06T15:10:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61", size = 134014, upload-time = "2026-05-06T15:10:08.213Z" }, + { url = "https://files.pythonhosted.org/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2", size = 141509, upload-time = "2026-05-06T15:10:09.595Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206", size = 415127, upload-time = "2026-05-06T15:10:11.049Z" }, + { url = "https://files.pythonhosted.org/packages/3a/dc/7446c538590d55f455647e5f3c61fc33f7108714e7afcffa6a2a033f8350/orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f", size = 148025, upload-time = "2026-05-06T15:10:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa", size = 136943, upload-time = "2026-05-06T15:10:14.405Z" }, + { url = "https://files.pythonhosted.org/packages/06/69/850264ccf6d80f6b174620d30a87f65c9b1490aba33fe6b62798e618cad3/orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470", size = 131606, upload-time = "2026-05-06T15:10:15.791Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/973a43fc9c55e20f2051e9830997649f669be0cb3ca52192087c0143f118/orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be", size = 127101, upload-time = "2026-05-06T15:10:17.129Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/495470f0e4a18f73fa10b7f6b84b464ec4cc5291c4e0c7c2a6c400bef006/orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624", size = 126736, upload-time = "2026-05-06T15:10:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" }, + { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" }, + { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" }, + { url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217, upload-time = "2026-05-06T15:10:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" }, + { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" }, + { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" }, + { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" }, + { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031, upload-time = "2026-05-06T15:10:36.358Z" }, + { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" }, + { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" }, + { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" }, + { url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" }, + { url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" }, + { url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" }, + { url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" }, + { url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" }, + { url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "prometheus-client" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/73/f1334c29c2af4cd9dba6c7817e61b611bd0215e2eb5565c6064a4de18802/prometheus_client-0.26.0.tar.gz", hash = "sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b", size = 92910, upload-time = "2026-07-24T19:36:41.893Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/a3/b69efbf4143b5b9859b977770bbbabcc2796b702fa69dc40271e45cd5a56/prometheus_client-0.26.0-py3-none-any.whl", hash = "sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6", size = 64494, upload-time = "2026-07-24T19:36:40.854Z" }, +] + +[[package]] +name = "prometheus-fastapi-instrumentator" +version = "8.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prometheus-client" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/f4/cdcebf7094b03b99fba71ac8f56bd6f227973642662f49d272332d8419b3/prometheus_fastapi_instrumentator-8.1.0.tar.gz", hash = "sha256:b77f3043665e8d28e2bbd21017506195a43d9adf1d402d01bf95b494b7e560e1", size = 20492, upload-time = "2026-07-26T11:12:44.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/b9/91a2246e6cf01b7ccb14479803c8a50f9c258ae5c6a0f16ba3294820632b/prometheus_fastapi_instrumentator-8.1.0-py3-none-any.whl", hash = "sha256:b9f40b2cff3f7891ca0610b3ae4fc6ec723fd326b04bb659819aaeb821a0fc7d", size = 19649, upload-time = "2026-07-26T11:12:45.168Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" }, +] + +[[package]] +name = "pygithub" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyjwt", extra = ["crypto"] }, + { name = "pynacl" }, + { name = "requests" }, + { name = "typing-extensions" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/c3/8465a311197e16cf5ab68789fe689535e90f6b61ab524cc32a39e67237ae/pygithub-2.9.1.tar.gz", hash = "sha256:59771d7ff63d54d427be2e7d0dad2208dfffc2b0a045fec959263787739b611c", size = 2594989, upload-time = "2026-04-14T07:26:13.622Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/aa/81a5506f089a26338bff17535e4339b3b22049ebd1bcdeff756c4d7a7559/pygithub-2.9.1-py3-none-any.whl", hash = "sha256:2ec78fca30092d51a42d76f4ddb02131b6f0c666a35dfdf364cf302cdda115b9", size = 449710, upload-time = "2026-04-14T07:26:12.382Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pynacl" +version = "1.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/79/0e3c34dc3c4671f67d251c07aa8eb100916f250ee470df230b0ab89551b4/pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594", size = 390064, upload-time = "2026-01-01T17:31:57.264Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/23a26e931736e13b16483795c8a6b2f641bf6a3d5238c22b070a5112722c/pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0", size = 809370, upload-time = "2026-01-01T17:31:59.198Z" }, + { url = "https://files.pythonhosted.org/packages/87/74/8d4b718f8a22aea9e8dcc8b95deb76d4aae380e2f5b570cc70b5fd0a852d/pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9", size = 1408304, upload-time = "2026-01-01T17:32:01.162Z" }, + { url = "https://files.pythonhosted.org/packages/fd/73/be4fdd3a6a87fe8a4553380c2b47fbd1f7f58292eb820902f5c8ac7de7b0/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574", size = 844871, upload-time = "2026-01-01T17:32:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/55/ad/6efc57ab75ee4422e96b5f2697d51bbcf6cdcc091e66310df91fbdc144a8/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634", size = 1446356, upload-time = "2026-01-01T17:32:04.452Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/928ee9c4779caa0a915844311ab9fb5f99585621c5d6e4574538a17dca07/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88", size = 826814, upload-time = "2026-01-01T17:32:06.078Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a9/1bdba746a2be20f8809fee75c10e3159d75864ef69c6b0dd168fc60e485d/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14", size = 1411742, upload-time = "2026-01-01T17:32:07.651Z" }, + { url = "https://files.pythonhosted.org/packages/f3/2f/5e7ea8d85f9f3ea5b6b87db1d8388daa3587eed181bdeb0306816fdbbe79/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444", size = 801714, upload-time = "2026-01-01T17:32:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/06/ea/43fe2f7eab5f200e40fb10d305bf6f87ea31b3bbc83443eac37cd34a9e1e/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b", size = 1372257, upload-time = "2026-01-01T17:32:11.026Z" }, + { url = "https://files.pythonhosted.org/packages/4d/54/c9ea116412788629b1347e415f72195c25eb2f3809b2d3e7b25f5c79f13a/pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145", size = 231319, upload-time = "2026-01-01T17:32:12.46Z" }, + { url = "https://files.pythonhosted.org/packages/ce/04/64e9d76646abac2dccf904fccba352a86e7d172647557f35b9fe2a5ee4a1/pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590", size = 244044, upload-time = "2026-01-01T17:32:13.781Z" }, + { url = "https://files.pythonhosted.org/packages/33/33/7873dc161c6a06f43cda13dec67b6fe152cb2f982581151956fa5e5cdb47/pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2", size = 188740, upload-time = "2026-01-01T17:32:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" }, + { url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" }, + { url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, +] + +[[package]] +name = "pywin32" +version = "312" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/f5/10a6e845a00fc5e7afd0a988b744f403d4d57162a28d160a093c4d9322f0/pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c", size = 6362659, upload-time = "2026-06-04T07:49:21.349Z" }, + { url = "https://files.pythonhosted.org/packages/35/c4/dcd2d62b5944b6d5db53413a5899016ccd57ffcb7278f3f81655d25d2027/pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a", size = 6928825, upload-time = "2026-06-04T07:49:23.934Z" }, + { url = "https://files.pythonhosted.org/packages/b7/56/3cbb433fe4501cdba2eb9040f56a4e1a8243faa4186b25295564d1a7a79d/pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47", size = 6721875, upload-time = "2026-06-04T07:49:26.416Z" }, + { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "readchar" +version = "4.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/49/a10341024c45bed95d13197ec9ef0f4e2fd10b5ca6e7f8d7684d18082398/readchar-4.2.2.tar.gz", hash = "sha256:e3b270fe16fc90c50ac79107700330a133dd4c63d22939f5b03b4f24564d5dd8", size = 9762, upload-time = "2026-04-06T19:45:54.226Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ca/36133653e00939922dd1416f4c56177361289172a30563fcb9552c9ccde4/readchar-4.2.2-py3-none-any.whl", hash = "sha256:92daf7e42c52b0787e6c75d01ecfb9a94f4ceff3764958b570c1dddedd47b200", size = 9401, upload-time = "2026-04-06T19:45:52.993Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rpds-py" +version = "2026.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" }, + { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" }, + { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" }, + { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" }, + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, + { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, + { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, + { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, + { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, + { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, + { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, + { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" }, + { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" }, + { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" }, + { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" }, + { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" }, +] + +[[package]] +name = "runs" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "xmod" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/ae/095cb626504733e288a81f871f86b10530b787d77c50193c170daaca0df1/runs-1.3.0.tar.gz", hash = "sha256:cca304b631dbefec598c7bfbcfb50d6feace6d3a968734b67fd42d3c728f5a05", size = 4585, upload-time = "2026-02-03T15:59:58.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/b6/049c75d399ccf6e25abea0652b85bf7e7e101e0300aa9c1d284ad7061c0b/runs-1.3.0-py3-none-any.whl", hash = "sha256:e71a551cfa8da9ef882cac1d5a108bda78c9edee5b8d87e37c1003da5b6a7bed", size = 6406, upload-time = "2026-02-03T15:59:59.96Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.4.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/00/b42a44342a054d58cb1115d7c8aa9cb4290dd9442f9c1b91a4b8173dba22/sse_starlette-3.4.8.tar.gz", hash = "sha256:ed89ffbb75cbf78a5fe2f2109cd584792ee7f9dfac96f791db546df8f15f3f9c", size = 32548, upload-time = "2026-08-05T11:19:49.982Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/3a/764912c58293d95b6dcdf4cc255f9d10de310580ced547b082eb9d72018c/sse_starlette-3.4.8-py3-none-any.whl", hash = "sha256:6e82314c786709a3cd9520f2285cf9fff90e181e598e8a357b0cf80f66afba0d", size = 16516, upload-time = "2026-08-05T11:19:48.748Z" }, +] + +[[package]] +name = "starlette" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.52.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/28/64ca011edf31c715b4fad359c587ea52391aaffa125065695590241ff617/uvicorn-0.52.3.tar.gz", hash = "sha256:18857b9e6579300be55c91c0a1cfd37d9a2cf0cabea33b88275f199eb73b8b58", size = 100621, upload-time = "2026-08-13T16:50:02.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/2b/ebd108734a8204c6b4b93c681c9a38c5273b3ccd5d129fee4ffc1d97772c/uvicorn-0.52.3-py3-none-any.whl", hash = "sha256:116af2710dbf47c80f463cd20ee4884b6662f4c9f227d797ddc7279d2fcc2c7c", size = 79859, upload-time = "2026-08-13T16:50:01.323Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, +] + +[[package]] +name = "xmod" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/3b/5a0d2670bab661164e27a5c27c448ae6204458c97cb94ccf89d0c47715bc/xmod-1.10.0.tar.gz", hash = "sha256:b40b2a54d56684b01eb9627892b0c179918e8ef0bd4d7f3bac7a3fdba11cd6e6", size = 17862, upload-time = "2026-05-09T13:46:46.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/e9/c362d47ed2b1928d65d61888c1419fae8ef69417fba2067d2fd3da1d400b/xmod-1.10.0-py3-none-any.whl", hash = "sha256:bebf8493b7ac63097401590a329c9ed20da224de0583a522e7ccb634af122f5a", size = 4661, upload-time = "2026-05-09T13:46:45.776Z" }, +] diff --git a/plugins/tests/output_length_guard/test_integration.py b/plugins/tests/output_length_guard/test_integration.py new file mode 100644 index 0000000..73dfec6 --- /dev/null +++ b/plugins/tests/output_length_guard/test_integration.py @@ -0,0 +1,284 @@ +"""Plugin-framework integration tests for output_length_guard. + +Tests the full plugin stack: PyO3 bindings → Python shim → cpex framework. +Run via: make test-integration from the plugin directory. +""" +from __future__ import annotations + +from pathlib import Path + +import pytest + +from real_cpex_imports import assert_real_cpex_imports +from cpex.framework import ( + PluginConfig, + PluginContext, + ToolPostInvokePayload, +) +from cpex.framework.extensions import Extensions, RequestExtension +from cpex.framework.models import GlobalContext + +from cpex_output_length_guard.output_length_guard import OutputLengthGuardPlugin + + +def test_imports_with_real_cpex_package() -> None: + plugin_root = ( + Path(__file__).resolve().parents[3] + / "plugins" + / "rust" + / "python-package" + / "output_length_guard" + ) + assert_real_cpex_imports( + plugin_root, + [ + "from cpex_output_length_guard.output_length_guard import OutputLengthGuardPlugin", + ], + ) + + +def _make_config(**overrides) -> PluginConfig: + config: dict = { + "max_chars": 100, + "strategy": "truncate", + "limit_mode": "character", + } + config.update(overrides) + return PluginConfig( + name="output_length_guard", + kind="cpex_output_length_guard.output_length_guard.OutputLengthGuardPlugin", + config=config, + ) + + +def _make_context() -> PluginContext: + return PluginContext( + global_context=GlobalContext( + request_id="req-olg", server_id="srv-olg" + ) + ) + + +def test_plugin_instantiates() -> None: + plugin = OutputLengthGuardPlugin(_make_config()) + assert plugin is not None + + +def test_invalid_strategy_raises() -> None: + with pytest.raises((ValueError, Exception)): + OutputLengthGuardPlugin(_make_config(strategy="skip")) + + +def test_invalid_limit_mode_raises() -> None: + with pytest.raises((ValueError, Exception)): + OutputLengthGuardPlugin(_make_config(limit_mode="bytes")) + + +@pytest.mark.asyncio +async def test_truncates_long_plain_string() -> None: + plugin = OutputLengthGuardPlugin(_make_config(max_chars=10)) + payload = ToolPostInvokePayload(name="tool1", result="A" * 100) + result = await plugin.tool_post_invoke(payload, _make_context()) + assert result.modified_payload is not None + assert len(result.modified_payload.result) <= 10 + + +@pytest.mark.asyncio +async def test_short_string_passes_through_unchanged() -> None: + plugin = OutputLengthGuardPlugin(_make_config(max_chars=1000)) + payload = ToolPostInvokePayload(name="tool1", result="hello") + result = await plugin.tool_post_invoke(payload, _make_context()) + assert result.modified_payload is None + + +@pytest.mark.asyncio +async def test_blocks_long_string_in_block_mode() -> None: + plugin = OutputLengthGuardPlugin(_make_config(max_chars=10, strategy="block")) + payload = ToolPostInvokePayload(name="tool1", result="A" * 100) + result = await plugin.tool_post_invoke(payload, _make_context()) + assert result.continue_processing is False + assert result.violation is not None + assert result.violation.code == "OUTPUT_LENGTH_VIOLATION" + + +@pytest.mark.asyncio +async def test_numeric_string_passes_through_unchanged_even_in_block_mode() -> None: + plugin = OutputLengthGuardPlugin(_make_config(max_chars=2, strategy="block")) + payload = ToolPostInvokePayload(name="tool1", result="42") + result = await plugin.tool_post_invoke(payload, _make_context()) + assert result.continue_processing is True + + +@pytest.mark.asyncio +async def test_dict_with_text_field_is_truncated() -> None: + plugin = OutputLengthGuardPlugin(_make_config(max_chars=10)) + payload = ToolPostInvokePayload( + name="tool1", result={"text": "A very long string that exceeds the limit"} + ) + result = await plugin.tool_post_invoke(payload, _make_context()) + assert result.modified_payload is not None + assert len(result.modified_payload.result["text"]) <= 10 + + +@pytest.mark.asyncio +async def test_dict_without_text_field_passes_through() -> None: + plugin = OutputLengthGuardPlugin(_make_config(max_chars=5)) + payload = ToolPostInvokePayload(name="t", result={"other": "value"}) + result = await plugin.tool_post_invoke(payload, _make_context()) + assert result.modified_payload is None + + +@pytest.mark.asyncio +async def test_mcp_content_array_text_item_is_truncated() -> None: + plugin = OutputLengthGuardPlugin(_make_config(max_chars=10)) + payload = ToolPostInvokePayload( + name="t", + result=[{"type": "text", "text": "A" * 100}], + ) + result = await plugin.tool_post_invoke(payload, _make_context()) + assert result.modified_payload is not None + assert len(result.modified_payload.result[0]["text"]) <= 10 + + +@pytest.mark.asyncio +async def test_mcp_content_dict_with_content_array_is_truncated() -> None: + plugin = OutputLengthGuardPlugin(_make_config(max_chars=10)) + payload = ToolPostInvokePayload( + name="t", + result={ + "content": [{"type": "text", "text": "A" * 100}], + "isError": False, + }, + ) + result = await plugin.tool_post_invoke(payload, _make_context()) + assert result.modified_payload is not None + content = result.modified_payload.result["content"] + assert len(content[0]["text"]) <= 10 + + +@pytest.mark.asyncio +async def test_string_list_is_truncated() -> None: + plugin = OutputLengthGuardPlugin(_make_config(max_chars=5)) + payload = ToolPostInvokePayload(name="t", result=["hello world", "hi"]) + result = await plugin.tool_post_invoke(payload, _make_context()) + assert result.modified_payload is not None + assert len(result.modified_payload.result[0]) <= 5 + assert result.modified_payload.result[1] == "hi" + + +@pytest.mark.asyncio +async def test_word_boundary_truncation() -> None: + plugin = OutputLengthGuardPlugin( + _make_config(max_chars=20, word_boundary=True, ellipsis="…") + ) + payload = ToolPostInvokePayload( + name="t", result="The quick brown fox jumps over the lazy dog" + ) + result = await plugin.tool_post_invoke(payload, _make_context()) + assert result.modified_payload is not None + new_text = result.modified_payload.result + # Result should respect character limit + assert len(new_text) <= 20 + + +@pytest.mark.asyncio +async def test_token_mode_truncates_by_token_budget() -> None: + plugin = OutputLengthGuardPlugin( + _make_config( + max_chars=None, + max_tokens=2, + limit_mode="token", + chars_per_token=4, + ) + ) + payload = ToolPostInvokePayload( + name="t", result="abcdefghijklmnop" # 16 chars = 4 estimated tokens + ) + result = await plugin.tool_post_invoke(payload, _make_context()) + assert result.modified_payload is not None + + +@pytest.mark.asyncio +async def test_metrics_emitted_when_trace_id_present() -> None: + plugin = OutputLengthGuardPlugin(_make_config(max_chars=10)) + ext = Extensions(request=RequestExtension(trace_id="t1")) + payload = ToolPostInvokePayload(name="t", result="A" * 100) + result = await plugin.tool_post_invoke(payload, _make_context(), ext) + assert result.modified_payload is not None + assert result.metadata is not None + metrics = result.metadata.get("output_length_guard") + assert metrics is not None + assert "chars_seen" in metrics + assert "truncated_count" in metrics + assert "blocked" in metrics + assert "limit_mode" in metrics + assert "strategy" in metrics + assert "stage" in metrics + + +@pytest.mark.asyncio +async def test_metrics_not_emitted_without_trace_id() -> None: + plugin = OutputLengthGuardPlugin(_make_config(max_chars=10)) + payload = ToolPostInvokePayload(name="t", result="A" * 100) + result = await plugin.tool_post_invoke(payload, _make_context()) + if result.metadata: + assert "output_length_guard" not in result.metadata + + +@pytest.mark.asyncio +async def test_no_raw_content_in_metrics() -> None: + """Verify metrics carry no raw text content.""" + plugin = OutputLengthGuardPlugin(_make_config(max_chars=10)) + ext = Extensions(request=RequestExtension(trace_id="t1")) + oversized_text = "SENSITIVE_DATA_" * 10 + payload = ToolPostInvokePayload(name="t", result=oversized_text) + result = await plugin.tool_post_invoke(payload, _make_context(), ext) + if result.metadata: + flat = str(result.metadata) + assert "SENSITIVE_DATA_" not in flat + + +@pytest.mark.asyncio +async def test_hook_backward_compatible_without_extensions() -> None: + plugin = OutputLengthGuardPlugin(_make_config(max_chars=1000)) + payload = ToolPostInvokePayload(name="t", result="hello") + result = await plugin.tool_post_invoke(payload, _make_context()) # 2-arg call + assert result is not None + + +@pytest.mark.asyncio +async def test_security_limit_max_structure_size_block() -> None: + plugin = OutputLengthGuardPlugin( + _make_config(max_chars=10000, strategy="block", max_structure_size=2) + ) + payload = ToolPostInvokePayload( + name="t", + result={"content": [{"type": "text", "text": "a"}, {"type": "text", "text": "b"}, {"type": "text", "text": "c"}]}, + ) + result = await plugin.tool_post_invoke(payload, _make_context()) + # Should block due to oversized content list + assert result.continue_processing is False + assert result.violation is not None + assert result.violation.code == "STRUCTURE_SIZE_VIOLATION" + + +@pytest.mark.asyncio +async def test_security_limit_max_recursion_depth_block() -> None: + plugin = OutputLengthGuardPlugin( + _make_config(max_chars=10000, strategy="block", max_recursion_depth=10) + ) + # Build deeply nested dict + nested: dict = {"value": "leaf"} + for _ in range(15): + nested = {"child": nested} + payload = ToolPostInvokePayload( + name="t", + result={ + "content": [{"type": "text", "text": str(nested)}], + "structuredContent": nested, + }, + ) + result = await plugin.tool_post_invoke(payload, _make_context()) + assert result.continue_processing is False + assert result.violation is not None + assert result.violation.code == "STRUCTURE_DEPTH_VIOLATION" diff --git a/plugins/tests/plugin_hooks.py b/plugins/tests/plugin_hooks.py index 72fd11b..371b246 100644 --- a/plugins/tests/plugin_hooks.py +++ b/plugins/tests/plugin_hooks.py @@ -164,6 +164,7 @@ class PluginViolation: description: str = "" code: str = "" details: dict[str, Any] | None = None + mcp_error_code: int | None = None http_status_code: int = 400 http_headers: dict[str, str] | None = None plugin_name: str = "" diff --git a/tests/test_plugin_catalog.py b/tests/test_plugin_catalog.py index 2fc582d..11cd400 100644 --- a/tests/test_plugin_catalog.py +++ b/tests/test_plugin_catalog.py @@ -572,6 +572,7 @@ def test_repo_lists_all_managed_plugins(self) -> None: { "encoded_exfil_detection", "ica_metering_exporter", + "output_length_guard", "pii_filter", "rate_limiter", "retry_with_backoff", @@ -586,6 +587,7 @@ def test_repo_lists_all_managed_plugins(self) -> None: { "encoded_exfil_detection": "cpex_encoded_exfil_detection", "ica_metering_exporter": "cpex_ica_metering_exporter", + "output_length_guard": "cpex_output_length_guard", "pii_filter": "cpex_pii_filter", "rate_limiter": "cpex_rate_limiter", "retry_with_backoff": "cpex_retry_with_backoff", @@ -599,6 +601,7 @@ def test_repo_lists_all_managed_plugins(self) -> None: { "encoded_exfil_detection": "cpex_encoded_exfil_detection.encoded_exfil_detection.EncodedExfilDetectorPlugin", "ica_metering_exporter": "cpex_ica_metering_exporter.plugin.IcaMeteringExporterPlugin", + "output_length_guard": "cpex_output_length_guard.output_length_guard.OutputLengthGuardPlugin", "pii_filter": "cpex_pii_filter.pii_filter.PIIFilterPlugin", "rate_limiter": "cpex_rate_limiter.rate_limiter.RateLimiterPlugin", "retry_with_backoff": "cpex_retry_with_backoff.retry_with_backoff.RetryWithBackoffPlugin", @@ -2427,6 +2430,7 @@ def test_framework_bridge_mutation_dependents_match_real_manifests(self) -> None dependents, [ "encoded_exfil_detection", + "output_length_guard", "pii_filter", "rate_limiter", "retry_with_backoff", @@ -2527,6 +2531,7 @@ def test_framework_bridge_mutation_job_uses_real_dependents(self) -> None: "in_diff": True, "test_packages": [ "encoded_exfil_detection", + "output_length_guard", "pii_filter", "rate_limiter", "retry_with_backoff", @@ -2630,6 +2635,7 @@ def test_ci_selection_reports_mutation_package_for_single_rust_diff(self) -> Non def test_ci_selection_field_prints_json_and_bool_scalars(self) -> None: expected_rust_plugins = [ "encoded_exfil_detection", + "output_length_guard", "pii_filter", "rate_limiter", "retry_with_backoff", @@ -2652,7 +2658,7 @@ def test_ci_selection_field_prints_json_and_bool_scalars(self) -> None: result = run_catalog("ci-selection-field", str(REPO_ROOT), "all", "", "", "plugin_count") self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(result.stdout.strip(), "8") + self.assertEqual(result.stdout.strip(), "9") result = run_catalog("ci-selection-field", str(REPO_ROOT), "all", "", "", "cargo_packages") self.assertEqual(result.returncode, 0, result.stderr) @@ -2689,7 +2695,7 @@ def test_ci_selection_field_prints_json_and_bool_scalars(self) -> None: "python_plugins": '["ica_metering_exporter"]', "has_rust_plugins": "true", "has_python_plugins": "true", - "rust_plugin_count": "7", + "rust_plugin_count": "8", "python_plugin_count": "1", "rust_release_validation_tags": "[]", "python_release_validation_tags": "[]", @@ -3458,9 +3464,10 @@ def test_ci_workflow_dispatch_detect_step_selects_all_plugins(self) -> None: if "=" in line ) self.assertEqual(outputs["has_plugins"], "true") - self.assertEqual(outputs["plugin_count"], "8") + self.assertEqual(outputs["plugin_count"], "9") expected_rust_plugins = [ "encoded_exfil_detection", + "output_length_guard", "pii_filter", "rate_limiter", "retry_with_backoff", @@ -4619,8 +4626,8 @@ def test_ci_selection_reports_language_splits_and_counts(self) -> None: self.assertEqual(result.returncode, 0, result.stderr) repository_payload = json.loads(result.stdout) - # Then all-mode characterizes the required 7 Rust / 1 Python split. - self.assertEqual(repository_payload["rust_plugin_count"], 7) + # Then all-mode characterizes the required 8 Rust / 1 Python split. + self.assertEqual(repository_payload["rust_plugin_count"], 8) self.assertEqual(repository_payload["python_plugin_count"], 1) self.assertTrue(repository_payload["has_rust_plugins"]) self.assertTrue(repository_payload["has_python_plugins"])