diff --git a/sdks/symphony-python/.gitignore b/sdks/symphony-python/.gitignore new file mode 100644 index 000000000..675134a6f --- /dev/null +++ b/sdks/symphony-python/.gitignore @@ -0,0 +1,6 @@ +.vscode/ +.venv/ +__pycache__/ +*.pyc +*.egg-info/ +dist/ diff --git a/sdks/symphony-python/README.md b/sdks/symphony-python/README.md new file mode 100644 index 000000000..a825e5cc5 --- /dev/null +++ b/sdks/symphony-python/README.md @@ -0,0 +1,435 @@ +# Symphony SDK for Python + +[![Python Version](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/) + +`symphony-sdk` is a lightweight, dependency-minimal Python client for interacting with [Eclipse Symphony](https://github.com/eclipse-symphony/symphony). It bundles the REST API client together with the COA (Cloud Object API) data models, type definitions, and summary helpers used by Symphony control planes and providers. + +## Features + +**Modern REST Client** +- Automatic token management and session handling +- Context manager support for clean resource management +- Comprehensive error handling with `SymphonyAPIError` +- Configurable timeouts and logging + +**Complete Data Models** +- COA request/response structures with multiple content types +- Target, Solution, Instance, and Deployment specifications +- Full type hints and dataclass support for IDE autocomplete +- Summary and status tracking models + +**Production Ready** +- Dependency-minimal: only requires `requests` +- Comprehensive test suite +- Detailed documentation and examples +- Type-safe with Python dataclasses + +## Installation + +Install from source: + +```bash +git clone https://github.com/eclipse-symphony/symphony.git +cd symphony/sdks/symphony-python +pip install -e . +``` + +## Quick Start + +### Basic Client Usage + +```python +from symphony_sdk import SymphonyAPI + +# Use context manager for automatic cleanup +with SymphonyAPI( + base_url="http://localhost:8082", + username="admin", + password="" +) as client: + # Authentication happens automatically + if client.health_check(): + print("Connected to Symphony!") + + # List all targets + targets = client.list_targets() + print(f"Found {len(targets.get('items', []))} targets") +``` + +### Working with Targets + +```python +from symphony_sdk import SymphonyAPI + +with SymphonyAPI(base_url, username, password) as client: + # Register a new target + target_spec = { + "displayName": "Edge Gateway", + "properties": { + "location": "datacenter-1", + "os": "linux" + } + } + client.register_target("gateway-001", target_spec) + + # Send heartbeat + client.ping_target("gateway-001") + + # Unregister when done + client.unregister_target("gateway-001") +``` + +### Deploying Solutions + +```python +import yaml +from symphony_sdk import SymphonyAPI + +with SymphonyAPI(base_url, username, password) as client: + # Create a solution + solution = { + "displayName": "Web App", + "components": [{ + "name": "nginx", + "type": "container", + "properties": {"image": "nginx:latest"} + }] + } + client.create_solution("web-app", yaml.dump(solution)) + + # Deploy an instance + instance_spec = { + "solution": "web-app", + "target": {"name": "gateway-001"} + } + client.create_instance("web-app-prod", instance_spec) + + # Check status + status = client.get_instance_status("web-app-prod") + print(f"Status: {status.get('status')}") +``` + +### Working with COA + +```python +from symphony_sdk import COARequest, COAResponse, State + +# Create a COA request +request = COARequest( + method="GET", + route="/components/list", + content_type="application/json" +) +request.set_body({"filter": "active"}) + +# Create responses +success = COAResponse.success({"components": ["nginx", "redis"]}) +error = COAResponse.error("Not found", State.NOT_FOUND) + +# Get decoded body +data = success.get_body() +print(data) # {'components': ['nginx', 'redis']} +``` + +### Using Type-Safe Dataclasses + +```python +from symphony_sdk import ( + TargetSpec, ComponentSpec, InstanceSpec, + TargetSelector, ObjectMeta +) + +# Create strongly-typed specifications +metadata = ObjectMeta( + name="my-target", + namespace="default", + labels={"env": "prod"} +) + +component = ComponentSpec( + name="nginx", + type="container", + properties={"image": "nginx:latest"} +) + +target = TargetSpec( + displayName="Production Gateway", + components=[component], + metadata={"owner": "platform-team"} +) + +instance = InstanceSpec( + name="my-app", + solution="web-app", + target=TargetSelector(name="my-target"), + parameters={"replicas": "3"} +) +``` + +## Documentation + +📚 **Complete Documentation:** +- [Quick Start Guide](docs/QUICKSTART.md) - Get up and running quickly +- [Glossary](docs/GLOSSARY.md) - Commonly used terms and their explanations +- [API Reference](docs/API.md) - API documentation +- [Examples](examples/) - Practical usage examples + +📖 **Examples:** +- [Basic Client Usage](examples/01_basic_client.py) - Authentication and health checks +- [Target Management](examples/02_target_management.py) - Register and manage targets +- [Solution Deployment](examples/03_solution_deployment.py) - Deploy solutions and instances +- [COA Provider](examples/04_coa_provider.py) - Work with COA requests/responses +- [Summary Tracking](examples/05_summary_tracking.py) - Track deployment status + +## Key Components + +### SymphonyAPI Client + +The main client for interacting with Symphony: + +```python +client = SymphonyAPI( + base_url="https://symphony.example.com", + username="user", + password="pass", + timeout=30.0, # Optional: custom timeout + logger=my_logger # Optional: custom logger +) +``` + +**Methods:** +- Target Management: `register_target()`, `unregister_target()`, `list_targets()`, `get_target()`, `ping_target()` +- Solution Management: `create_solution()`, `get_solution()`, `delete_solution()`, `list_solutions()` +- Instance Management: `create_instance()`, `get_instance()`, `delete_instance()`, `list_instances()` +- Deployment: `apply_deployment()`, `reconcile_solution()`, `get_instance_status()` +- Utilities: `authenticate()`, `health_check()`, + +### Data Models + +Comprehensive data models for Symphony resources: + +- **Target Models**: `TargetSpec`, `TargetState`, `TargetStatus`, `TargetSelector` +- **Solution Models**: `SolutionSpec`, `SolutionState`, `ComponentSpec`, `RouteSpec` +- **Instance Models**: `InstanceSpec`, `DeploymentSpec`, `TopologySpec`, `PipelineSpec` +- **COA Models**: `COARequest`, `COAResponse`, `State` +- **Summary Models**: `SummaryResult`, `SummarySpec`, `ComponentResultSpec`, `TargetResultSpec` +- **Metadata**: `ObjectMeta`, `BindingSpec`, `DeviceSpec` + +All models are Python dataclasses with full type hints. + +### Error Handling + +```python +from symphony_sdk import SymphonyAPIError + +try: + client.register_target("device-001", spec) +except SymphonyAPIError as e: + print(f"Error: {e}") + print(f"Status code: {e.status_code}") + print(f"Response: {e.response_text}") +``` + +## Development + +### Setup + +```bash +# Clone the repository +git clone https://github.com/eclipse-symphony/symphony.git +cd symphony/sdks/symphony-python +``` + +#### Using pip + +```bash +# Install in development mode with all development dependencies +pip install -e ".[dev]" +``` + +#### Using uv + +```bash +# Install the package and dev dependencies +uv pip install -e ".[dev]" +``` + +This installs the package along with: +- `pytest` and `pytest-cov` - Testing and coverage +- `ruff` - Fast linter and formatter +- `mypy` - Static type checker +- `types-PyYAML` and `types-requests` - Type stubs for dependencies + +### Running Tests + +#### Using pip + +```bash +# Run all tests +pytest + +# Run with coverage (configured in pyproject.toml) +pytest --cov=src/symphony_sdk --cov-report=html --cov-report=term-missing + +# Run specific test file +pytest tests/test_api_client.py + +# Run tests in verbose mode +pytest -v +``` + +#### Using uv + +```bash +# Run all tests +uv run pytest + +# Run with coverage (configured in pyproject.toml) +uv run pytest --cov=src/symphony_sdk --cov-report=html --cov-report=term-missing + +# Run specific test file +uv run pytest tests/test_api_client.py + +# Run tests in verbose mode +uv run pytest -v +``` + +### Code Quality Tools + +The project is configured with modern Python tooling for maintaining code quality: + +#### Linting and Formatting with Ruff + +```bash +# Check code for issues +ruff check . + +# Check code and auto-fix issues +ruff check --fix . + +# Format code +ruff format . + +# Check formatting without making changes +ruff format --check . +``` + +Ruff is configured in `pyproject.toml` to enforce: +- PEP 8 style guidelines +- Import sorting (isort-compatible) +- Google-style docstrings +- Modern Python idioms +- Common bug patterns + +#### Type Checking with Mypy + +```bash +# Type check the entire codebase +mypy src/ + +# Type check with verbose output +mypy --show-error-codes src/ + +# Type check a specific file +mypy src/symphony_sdk/api_client.py +``` + +The codebase has comprehensive type hints (77%+ coverage) and is configured for strict type checking in `pyproject.toml`. + +### Development Workflow + +Recommended workflow before committing: + +```bash +# 1. Format code +ruff format . + +# 2. Fix linting issues +ruff check --fix . + +# 3. Run type checker +mypy src/ + +# 4. Run tests with coverage +pytest + +# 5. Review coverage report +open htmlcov/index.html # or xdg-open on Linux +``` + +### Pre-commit Integration (Optional) + +To run checks automatically before each commit, install pre-commit hooks: + +```bash +# Install pre-commit +pip install pre-commit + +# Install the git hooks +pre-commit install +``` + +Create `.pre-commit-config.yaml`: + +```yaml +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.0 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.0.0 + hooks: + - id: mypy + additional_dependencies: [types-PyYAML, types-requests] +``` + +### Project Structure + +``` +symphony-python/ +├── src/symphony_sdk/ +│ ├── __init__.py # Public API exports +│ ├── api_client.py # REST API client +│ ├── models.py # Data models and COA structures +│ ├── types.py # State enum and constants +│ └── summary.py # Summary tracking models +├── tests/ # Unit tests +├── examples/ # Usage examples +├── docs/ # Documentation +├── README.md +├── CHANGELOG.md +└── pyproject.toml # Package configuration +``` + +## Requirements + +- Python 3.9 or higher +- `requests` library (automatically installed) + +Optional dependencies: +- `paho-mqtt>=2.0` (for MQTT support) + +## Contributing + +Contributions are welcome! Please follow the Symphony project contribution guidelines: + +1. Fork the repository +2. Create a feature branch +3. Make your changes with tests +4. Submit a pull request + +## Links + +- [Eclipse Symphony](https://github.com/eclipse-symphony/symphony) - Main Symphony repository +- [Symphony Documentation](https://github.com/eclipse-symphony/symphony/tree/main/docs) - Architecture and concepts +- [Issue Tracker](https://github.com/eclipse-symphony/symphony/issues) - Report bugs or request features + +## Support + +- 📖 Read the [documentation](docs/) +- 💬 Check existing [issues](https://github.com/eclipse-symphony/symphony/issues) +- 🐛 Report bugs in the [issue tracker](https://github.com/eclipse-symphony/symphony/issues/new) +- 💡 See [examples](examples/) for common patterns diff --git a/sdks/symphony-python/docs/API.md b/sdks/symphony-python/docs/API.md new file mode 100644 index 000000000..5b1d74f53 --- /dev/null +++ b/sdks/symphony-python/docs/API.md @@ -0,0 +1,686 @@ +# Symphony Python SDK - API Reference + +Complete API reference for the Symphony Python SDK. + +## Table of Contents + +- [SymphonyAPI Client](#symphonyapi-client) +- [Data Models](#data-models) +- [COA Types](#coa-types) +- [Summary Models](#summary-models) +- [Utility Functions](#utility-functions) +- [Exceptions](#exceptions) + +--- + +## SymphonyAPI Client + +The main client for interacting with the Symphony REST API. + +### Class: `SymphonyAPI` + +```python +SymphonyAPI( + base_url: str, + username: str, + password: str, + timeout: float = 30.0, + logger: Optional[logging.Logger] = None +) +``` + +**Parameters:** +- `base_url`: Base URL of the Symphony API (e.g., 'https://symphony.example.com') +- `username`: Symphony username for authentication +- `password`: Symphony password for authentication +- `timeout`: Request timeout in seconds (default: 30.0) +- `logger`: Optional logger instance + +**Context Manager:** +Supports context manager protocol for automatic session cleanup. + +```python +with SymphonyAPI(base_url, username, password) as client: + # Use client + pass +``` + +### Authentication Methods + +#### `authenticate(force_refresh: bool = False) -> str` + +Authenticate with Symphony API and return access token. + +**Parameters:** +- `force_refresh`: Force token refresh even if current token is valid + +**Returns:** Access token string + +**Raises:** `SymphonyAPIError` if authentication fails + +### Target Management Methods + +#### `register_target(target_name: str, target_spec: Dict[str, Any]) -> Dict[str, Any]` + +Register a target with Symphony. + +**Parameters:** +- `target_name`: Name of the target to register +- `target_spec`: Target specification dictionary + +**Returns:** API response data + +#### `unregister_target(target_name: str, direct: bool = False) -> Dict[str, Any]` + +Unregister a target from Symphony. + +**Parameters:** +- `target_name`: Name of the target to unregister +- `direct`: Whether to use direct delete + +**Returns:** API response data + +#### `get_target(target_name: str, doc_type: str = 'yaml', path: str = '$.spec') -> Dict[str, Any]` + +Get target specification. + +**Parameters:** +- `target_name`: Name of the target +- `doc_type`: Document type ('yaml' or 'json') +- `path`: JSONPath to extract from response + +**Returns:** Target specification data + +#### `list_targets() -> Dict[str, Any]` + +List all registered targets. + +**Returns:** List of targets + +#### `ping_target(target_name: str) -> Dict[str, Any]` + +Send heartbeat ping to target. + +**Parameters:** +- `target_name`: Name of the target to ping + +**Returns:** Ping response data + +#### `update_target_status(target_name: str, status_data: Dict[str, Any]) -> Dict[str, Any]` + +Update target status. + +**Parameters:** +- `target_name`: Name of the target +- `status_data`: Status information dictionary + +**Returns:** API response data + +### Solution Management Methods + +#### `create_solution(solution_name: str, solution_spec: str, embed_type: Optional[str] = None, embed_component: Optional[str] = None, embed_property: Optional[str] = None) -> Dict[str, Any]` + +Create a solution with embedded specification. + +**Parameters:** +- `solution_name`: Name of the solution +- `solution_spec`: Solution specification as text (usually YAML) +- `embed_type`: Optional embed type +- `embed_component`: Optional embed component +- `embed_property`: Optional embed property + +**Returns:** API response data + +#### `get_solution(solution_name: str, doc_type: str = 'yaml', path: str = '$.spec') -> Dict[str, Any]` + +Get solution specification. + +**Parameters:** +- `solution_name`: Name of the solution +- `doc_type`: Document type ('yaml' or 'json') +- `path`: JSONPath to extract from response + +**Returns:** Solution specification data + +#### `delete_solution(solution_name: str) -> Dict[str, Any]` + +Delete a solution. + +**Parameters:** +- `solution_name`: Name of the solution to delete + +**Returns:** API response data + +#### `list_solutions() -> Dict[str, Any]` + +List all solutions. + +**Returns:** List of solutions + +### Instance Management Methods + +#### `create_instance(instance_name: str, instance_spec: Dict[str, Any]) -> Dict[str, Any]` + +Create an instance. + +**Parameters:** +- `instance_name`: Name of the instance +- `instance_spec`: Instance specification dictionary + +**Returns:** API response data + +#### `get_instance(instance_name: str, doc_type: str = 'yaml', path: str = '$.spec') -> Dict[str, Any]` + +Get instance specification. + +**Parameters:** +- `instance_name`: Name of the instance +- `doc_type`: Document type ('yaml' or 'json') +- `path`: JSONPath to extract from response + +**Returns:** Instance specification data + +#### `delete_instance(instance_name: str) -> Dict[str, Any]` + +Delete an instance. + +**Parameters:** +- `instance_name`: Name of the instance to delete + +**Returns:** API response data + +#### `list_instances() -> Dict[str, Any]` + +List all instances. + +**Returns:** List of instances + +### Deployment Methods + +#### `apply_deployment(deployment_spec: Dict[str, Any]) -> Dict[str, Any]` + +Apply a deployment. + +**Parameters:** +- `deployment_spec`: Deployment specification dictionary + +**Returns:** API response data + +#### `get_deployment_components() -> Dict[str, Any]` + +Get deployment components. + +**Returns:** Components data + +#### `delete_deployment_components() -> Dict[str, Any]` + +Delete deployment components. + +**Returns:** API response data + +#### `reconcile_solution(deployment_spec: Dict[str, Any], delete: bool = False) -> Dict[str, Any]` + +Direct reconcile/delete deployment. + +**Parameters:** +- `deployment_spec`: Deployment specification dictionary +- `delete`: Whether this is a delete operation + +**Returns:** API response data + +#### `get_instance_status(instance_name: str) -> Dict[str, Any]` + +Get instance status. + +**Parameters:** +- `instance_name`: Name of the instance + +**Returns:** Instance status data + +### Utility Methods + +#### `health_check() -> bool` + +Perform a basic health check of the Symphony API. + +**Returns:** True if API is accessible, False otherwise + +#### `close()` + +Close the HTTP session. + +--- + +## Data Models + +### ObjectMeta + +Kubernetes-style metadata for resources. + +```python +@dataclass +class ObjectMeta: + namespace: str = "" + name: str = "" + labels: Optional[Dict[str, str]] = None + annotations: Optional[Dict[str, str]] = None +``` + +### TargetSpec + +Specification for a Symphony target. + +```python +@dataclass +class TargetSpec: + properties: Dict[str, str] = None + components: List[ComponentSpec] = None + constraints: str = "" + topologies: List[TopologySpec] = None + scope: str = "" + displayName: str = "" + metadata: Dict[str, str] = None + forceRedeploy: bool = False +``` + +### ComponentSpec + +Specification for a component. + +```python +@dataclass +class ComponentSpec: + name: str = "" + type: str = "" + routes: List[RouteSpec] = None + constraints: str = "" + properties: Dict[str, str] = None + dependencies: List[str] = None + skills: List[str] = None + metadata: Dict[str, str] = None + parameters: Dict[str, str] = None +``` + +### SolutionSpec + +Specification for a solution. + +```python +@dataclass +class SolutionSpec: + components: List[ComponentSpec] = None + scope: str = "" + displayName: str = "" + metadata: Dict[str, str] = None +``` + +### InstanceSpec + +Specification for an instance deployment. + +```python +@dataclass +class InstanceSpec: + name: str = "" + parameters: Optional[Dict[str, str]] = None + solution: str = "" + target: Optional[TargetSelector] = None + topologies: Optional[List[TopologySpec]] = None + pipelines: Optional[List[PipelineSpec]] = None + scope: str = "" + display_name: str = "" + metadata: Optional[Dict[str, str]] = None + versions: Optional[List[VersionSpec]] = None + arguments: Optional[Dict[str, Dict[str, str]]] = None + opt_out_reconciliation: bool = False +``` + +### DeploymentSpec + +Complete deployment specification. + +```python +@dataclass +class DeploymentSpec: + solutionName: str = "" + solution: SolutionState = None + instance: InstanceSpec = None + targets: Dict[str, TargetState] = None + devices: List[DeviceSpec] = None + assignments: Dict[str, str] = None + componentStartIndex: int = -1 + componentEndIndex: int = -1 + activeTarget: str = "" + + def get_components_slice(self) -> List[ComponentSpec]: + """Get slice of components for deployment.""" +``` + +--- + +## COA Types + +### State + +Enumeration of COA response states. + +```python +class State(IntEnum): + # HTTP states + OK = 200 + ACCEPTED = 202 + BAD_REQUEST = 400 + UNAUTHORIZED = 401 + FORBIDDEN = 403 + NOT_FOUND = 404 + INTERNAL_ERROR = 500 + + # Custom states + BAD_CONFIG = 1000 + INVALID_ARGUMENT = 2000 + # ... and many more + + def __str__(self) -> str: + """Human-readable string representation.""" + + @classmethod + def from_http_status(cls, code: int) -> 'State': + """Get State from HTTP status code.""" +``` + +### COARequest + +COA request structure. + +```python +@dataclass +class COARequest(COABodyMixin): + method: str = "GET" + route: str = "" + metadata: Optional[Dict[str, str]] = field(default_factory=dict) + parameters: Optional[Dict[str, str]] = field(default_factory=dict) + content_type: str = "application/json" + body: str = "" # Base64 encoded + + def set_body(self, data: Any, content_type: Optional[str] = None) -> None: + """Set body data with content type.""" + + def get_body(self) -> Any: + """Get decoded body data.""" + + def to_json_dict(self) -> Dict[str, Any]: + """Convert to JSON-serializable dictionary.""" +``` + +### COAResponse + +COA response structure. + +```python +@dataclass +class COAResponse(COABodyMixin): + state: State = State.OK + metadata: Optional[Dict[str, str]] = field(default_factory=dict) + redirect_uri: Optional[str] = None + content_type: str = "application/json" + body: str = "" # Base64 encoded + + def set_body(self, data: Any, content_type: Optional[str] = None) -> None: + """Set body data with content type.""" + + def get_body(self) -> Any: + """Get decoded body data.""" + + def to_json_dict(self) -> Dict[str, Any]: + """Convert to JSON-serializable dictionary.""" + + @classmethod + def success(cls, data: Any = None, content_type: str = "application/json") -> 'COAResponse': + """Create a success response.""" + + @classmethod + def error(cls, message: str, state: State = State.INTERNAL_ERROR, + content_type: str = "application/json") -> 'COAResponse': + """Create an error response.""" + + @classmethod + def not_found(cls, message: str = "Resource not found") -> 'COAResponse': + """Create a not found response.""" + + @classmethod + def bad_request(cls, message: str = "Bad request") -> 'COAResponse': + """Create a bad request response.""" +``` + +--- + +## Summary Models + +### SummaryState + +State enumeration for summary operations. + +```python +class SummaryState(IntEnum): + PENDING = 0 # Currently unused + RUNNING = 1 # Reconcile operation in progress + DONE = 2 # Reconcile operation completed +``` + +### ComponentResultSpec + +Result specification for a component. + +```python +@dataclass +class ComponentResultSpec: + status: State = State.OK + message: str = "" + + def to_dict(self) -> Dict[str, any]: + """Convert to dictionary.""" + + @classmethod + def from_dict(cls, data: Dict[str, any]) -> 'ComponentResultSpec': + """Create from dictionary.""" +``` + +### TargetResultSpec + +Result specification for a target. + +```python +@dataclass +class TargetResultSpec: + status: str = "OK" + message: str = "" + component_results: Dict[str, ComponentResultSpec] = field(default_factory=dict) + + def to_dict(self) -> Dict[str, any]: + """Convert to dictionary.""" + + @classmethod + def from_dict(cls, data: Dict[str, any]) -> 'TargetResultSpec': + """Create from dictionary.""" +``` + +### SummarySpec + +Deployment summary specification. + +```python +@dataclass +class SummarySpec: + target_count: int = 0 + success_count: int = 0 + planned_deployment: int = 0 + current_deployed: int = 0 + target_results: Dict[str, TargetResultSpec] = field(default_factory=dict) + summary_message: str = "" + job_id: str = "" + skipped: bool = False + is_removal: bool = False + all_assigned_deployed: bool = False + removed: bool = False + + def update_target_result(self, target: str, spec: TargetResultSpec) -> None: + """Update target result, merging with existing.""" + + def generate_status_message(self) -> str: + """Generate detailed status message.""" + + def to_dict(self) -> Dict[str, any]: + """Convert to dictionary.""" + + @classmethod + def from_dict(cls, data: Dict[str, any]) -> 'SummarySpec': + """Create from dictionary.""" +``` + +### SummaryResult + +Complete summary result for a deployment. + +```python +@dataclass +class SummaryResult: + summary: SummarySpec = field(default_factory=SummarySpec) + summary_id: str = "" + generation: str = "" + time: datetime = field(default_factory=datetime.now) + state: SummaryState = SummaryState.PENDING + deployment_hash: str = "" + + def is_deployment_finished(self) -> bool: + """Check if deployment is finished.""" + + def to_dict(self) -> Dict[str, any]: + """Convert to dictionary.""" + + @classmethod + def from_dict(cls, data: Dict[str, any]) -> 'SummaryResult': + """Create from dictionary.""" +``` + +--- + +## Utility Functions + +### Serialization Functions + +```python +def to_dict(obj: Any) -> Dict[str, Any]: + """Convert dataclass object to dictionary.""" + +def from_dict(data: Dict[str, Any], cls: type) -> Any: + """Convert dictionary to dataclass object.""" + +def serialize_components(components: List[ComponentSpec]) -> str: + """Serialize components to JSON string.""" + +def deserialize_components(json_str: str) -> List[ComponentSpec]: + """Deserialize JSON string to components list.""" + +def serialize_coa_request(coa_request: COARequest) -> str: + """Serialize COARequest to JSON string.""" + +def deserialize_coa_request(json_str: str) -> COARequest: + """Deserialize JSON string to COARequest.""" + +def serialize_coa_response(coa_response: COAResponse) -> str: + """Serialize COAResponse to JSON string.""" + +def deserialize_coa_response(json_str: str) -> COAResponse: + """Deserialize JSON string to COAResponse.""" +``` + +### Summary Helper Functions + +```python +def create_success_component_result(message: str = "") -> ComponentResultSpec: + """Create a successful component result.""" + +def create_failed_component_result(message: str, status: State = None) -> ComponentResultSpec: + """Create a failed component result.""" + +def create_target_result(status: str = "OK", message: str = "", + component_results: Dict[str, ComponentResultSpec] = None) -> TargetResultSpec: + """Create a target result specification.""" +``` + +--- + +## Exceptions + +### SymphonyAPIError + +Custom exception for Symphony API errors. + +```python +class SymphonyAPIError(Exception): + def __init__(self, message: str, + status_code: Optional[int] = None, + response_text: Optional[str] = None): + """ + Initialize Symphony API error. + + Args: + message: Error message + status_code: HTTP status code if available + response_text: Raw response text if available + """ +``` + +**Attributes:** +- `message`: Error message (from `str(exception)`) +- `status_code`: HTTP status code (if available) +- `response_text`: Raw response text (if available) + +**Usage:** + +```python +try: + client.register_target("device", spec) +except SymphonyAPIError as e: + print(f"Error: {e}") + print(f"Status: {e.status_code}") + print(f"Details: {e.response_text}") +``` + +--- + +## Constants + +### COAConstants + +Constants used in Symphony COA operations. + +```python +class COAConstants: + # Header constants + COA_META_HEADER = "COA_META_HEADER" + + # Tracing and monitoring + TRACING_EXPORTER_CONSOLE = "tracing.exporters.console" + METRICS_EXPORTER_OTLP_GRPC = "metrics.exporters.otlpgrpc" + # ... more constants + + # Provider constants + PROVIDERS_PERSISTENT_STATE = "providers.persistentstate" + PROVIDERS_VOLATILE_STATE = "providers.volatilestate" + # ... more constants + + # Output constants + STATUS_OUTPUT = "status" + ERROR_OUTPUT = "error" + STATE_OUTPUT = "__state" +``` + +--- + +## See Also + +- [Quick Start Guide](QUICKSTART.md) +- [Examples](../examples/) +- [Main README](../README.md) +- [Symphony Documentation](https://github.com/eclipse-symphony/symphony) diff --git a/sdks/symphony-python/docs/FEATURES.md b/sdks/symphony-python/docs/FEATURES.md new file mode 100644 index 000000000..84016298c --- /dev/null +++ b/sdks/symphony-python/docs/FEATURES.md @@ -0,0 +1,720 @@ +# Symphony Python SDK - Features Overview + +This document provides a comprehensive overview of all features available in the Symphony Python SDK. + +## Table of Contents + +- [Client Features](#client-features) +- [Authentication & Security](#authentication--security) +- [Target Management](#target-management) +- [Solution Management](#solution-management) +- [Instance & Deployment](#instance--deployment) +- [COA (Cloud Object API)](#coa-cloud-object-api) +- [Status & Summary Tracking](#status--summary-tracking) +- [Data Models & Type Safety](#data-models--type-safety) +- [Error Handling](#error-handling) +- [Advanced Features](#advanced-features) + +--- + +## Client Features + +### SymphonyAPI Client + +The main client for all Symphony operations. + +**Features:** +- ✅ Context manager support for automatic cleanup +- ✅ Configurable timeout per request +- ✅ Custom logger integration +- ✅ Automatic session management and connection pooling +- ✅ Thread-safe request handling + +**Example:** +```python +with SymphonyAPI(base_url, username, password, timeout=60) as client: + # Auto cleanup when done + pass +``` + +### Connection Management + +**Features:** +- ✅ Automatic connection pooling via requests.Session +- ✅ Health check endpoint support +- ✅ Graceful connection closure +- ✅ Network error handling + +**Example:** +```python +if client.health_check(): + print("Symphony is reachable") +``` + +--- + +## Authentication & Security + +### Token-Based Authentication + +**Features:** +- ✅ Automatic token acquisition +- ✅ Token caching with expiry tracking +- ✅ Automatic token refresh +- ✅ Force refresh capability +- ✅ Secure credential handling + +**Example:** +```python +# Authentication happens automatically +client.list_targets() # Token acquired if needed + +# Or force refresh +token = client.authenticate(force_refresh=True) +``` + +### Security + +**Features:** +- ✅ Bearer token authentication +- ✅ HTTPS support +- ✅ No credential storage on disk +- ✅ Session-based credential management + +--- + +## Target Management + +Comprehensive target lifecycle management. + +### Registration + +**Features:** +- ✅ Register new targets with full spec +- ✅ Support for properties, components, metadata +- ✅ Topology and constraint definitions +- ✅ Force redeploy option + +**Example:** +```python +target_spec = { + "displayName": "Edge Gateway", + "properties": {"location": "dc1", "os": "linux"}, + "components": [...], + "metadata": {"owner": "team-a"} +} +client.register_target("gateway-001", target_spec) +``` + +### Querying + +**Features:** +- ✅ List all targets +- ✅ Get specific target details +- ✅ Support for YAML and JSON formats +- ✅ JSONPath filtering + +**Example:** +```python +# List all +targets = client.list_targets() + +# Get specific target +target = client.get_target("gateway-001") + +``` + +### Status Updates + +**Features:** +- ✅ Update target status +- ✅ Send heartbeat pings +- ✅ Track last seen time +- ✅ Custom status properties + +**Example:** +```python +# Heartbeat +client.ping_target("gateway-001") + +# Update status +client.update_target_status("gateway-001", { + "properties": {"health": "healthy"} +}) +``` + +### Unregistration + +**Features:** +- ✅ Graceful unregistration +- ✅ Direct delete option +- ✅ Cleanup of associated resources + +**Example:** +```python +client.unregister_target("gateway-001", direct=True) +``` + +--- + +## Solution Management + +Define and manage application solutions. + +### Creation + +**Features:** +- ✅ YAML-based solution definitions +- ✅ Multiple component support +- ✅ Embedded specifications +- ✅ Component properties and routing + +**Example:** +```python +solution_yaml = """ +displayName: Web Application +components: + - name: nginx + type: container + properties: + image: nginx:latest +""" +client.create_solution("web-app", solution_yaml) +``` + +### Components + +**Features:** +- ✅ Container components +- ✅ Config components +- ✅ Custom component types +- ✅ Component dependencies +- ✅ Component routing and filtering + +**Example:** +```python +component = ComponentSpec( + name="nginx", + type="container", + properties={"image": "nginx:latest"}, + dependencies=["config-service"], + routes=[...] +) +``` + +### Querying + +**Features:** +- ✅ List all solutions +- ✅ Get solution details +- ✅ YAML/JSON format support +- ✅ Component filtering + +**Example:** +```python +solutions = client.list_solutions() +solution = client.get_solution("web-app") +``` + +--- + +## Instance & Deployment + +Deploy solutions to targets. + +### Instance Creation + +**Features:** +- ✅ Create instances from solutions +- ✅ Target selection with selectors +- ✅ Custom parameters +- ✅ Topology definitions +- ✅ Pipeline configurations +- ✅ Version management + +**Example:** +```python +instance_spec = { + "solution": "web-app", + "target": {"name": "gateway-001"}, + "parameters": {"replicas": "3"}, + "topologies": [...], + "pipelines": [...] +} +client.create_instance("web-app-prod", instance_spec) +``` + +### Deployment Operations + +**Features:** +- ✅ Apply deployments +- ✅ Get deployment components +- ✅ Direct reconciliation +- ✅ Delete deployments + +**Example:** +```python +# Apply deployment +deployment_spec = {...} +client.apply_deployment(deployment_spec) + +# Reconcile +client.reconcile_solution(deployment_spec, delete=False) + +# Get components +components = client.get_deployment_components() +``` + +### Status Tracking + +**Features:** +- ✅ Real-time instance status +- ✅ Deployment progress tracking +- ✅ Success/failure detection + +**Example:** +```python +status = client.get_instance_status("web-app-prod") +if status.get('status') == 'Succeeded': + print("Deployment successful!") +``` + +--- + +## COA (Cloud Object API) + +Unified interface for provider operations. + +### COA Requests + +**Features:** +- ✅ Multiple HTTP methods (GET, POST, PUT, DELETE) +- ✅ Flexible routing +- ✅ Custom parameters and metadata +- ✅ Multiple content types: + - JSON (application/json) + - Plain text (text/plain) + - Binary (application/octet-stream) +- ✅ Automatic base64 encoding/decoding + +**Example:** +```python +request = COARequest( + method="POST", + route="/components/deploy", + content_type="application/json" +) +request.set_body({"component": "nginx", "version": "latest"}) + +# Get decoded body +data = request.get_body() +``` + +### COA Responses + +**Features:** +- ✅ Rich state enum (200+ states) +- ✅ Convenience methods for common responses +- ✅ Metadata support +- ✅ Redirect URI support +- ✅ Body encoding/decoding + +**Example:** +```python +# Success response +response = COAResponse.success({"status": "deployed"}) + +# Error responses +error = COAResponse.error("Failed", State.INTERNAL_ERROR) +not_found = COAResponse.not_found("Component not found") +bad_request = COAResponse.bad_request("Invalid input") +``` + +### State Management + +**Features:** +- ✅ Comprehensive state enum with 100+ values +- ✅ HTTP status code mapping +- ✅ Human-readable state strings +- ✅ Custom state codes for Symphony operations + +**Example:** +```python +state = State.from_http_status(404) # NOT_FOUND +print(str(state)) # "Not Found" + +if state == State.NOT_FOUND: + print("Resource not found") +``` + +### Serialization + +**Features:** +- ✅ JSON serialization/deserialization +- ✅ Content type handling +- ✅ Base64 encoding for binary data + +**Example:** +```python +# Serialize +json_str = serialize_coa_request(request) + +# Deserialize +request = deserialize_coa_request(json_str) +``` + +--- + +## Status & Summary Tracking + +Track deployment status and generate summaries. + +### Summary Specifications + +**Features:** +- ✅ Target-level tracking +- ✅ Component-level tracking +- ✅ Success/failure counters +- ✅ Deployment progress metrics +- ✅ Custom messages + +**Example:** +```python +summary = SummarySpec( + target_count=3, + success_count=3, + planned_deployment=10, + current_deployed=10, + all_assigned_deployed=True +) +``` + +### Component Results + +**Features:** +- ✅ Per-component status +- ✅ Success/failure tracking +- ✅ Error messages +- ✅ State codes + +**Example:** +```python +# Success +comp_result = create_success_component_result("Deployed") + +# Failure +comp_result = create_failed_component_result( + "Image pull failed", + State.INTERNAL_ERROR +) +``` + +### Target Results + +**Features:** +- ✅ Aggregate target status +- ✅ Component result collection +- ✅ Target-level messages +- ✅ Result merging + +**Example:** +```python +target_result = create_target_result( + status="OK", + message="All components deployed", + component_results={ + "nginx": success_result, + "redis": success_result + } +) + +summary.update_target_result("target-1", target_result) +``` + +### Summary Results + +**Features:** +- ✅ Complete deployment summary +- ✅ State tracking (PENDING, RUNNING, DONE) +- ✅ Timestamp tracking +- ✅ Deployment hash +- ✅ Detailed status message generation +- ✅ Serialization support + +**Example:** +```python +result = SummaryResult( + summary=summary, + summary_id="deploy-001", + state=SummaryState.DONE, + deployment_hash="abc123" +) + +if result.is_deployment_finished(): + message = result.summary.generate_status_message() +``` + +--- + +## Data Models & Type Safety + +Comprehensive data models with full type hints. + +### Resource Metadata + +**Features:** +- ✅ Kubernetes-style metadata +- ✅ Namespace support +- ✅ Labels and annotations +- ✅ Resource naming + +**Example:** +```python +metadata = ObjectMeta( + name="my-resource", + namespace="default", + labels={"env": "prod", "app": "web"}, + annotations={"owner": "team-a"} +) +``` + +### Specifications + +**Features:** +- ✅ TargetSpec with full target definition +- ✅ SolutionSpec with components +- ✅ InstanceSpec with deployment config +- ✅ DeploymentSpec with complete state +- ✅ ComponentSpec with properties and routing + +### Type Conversion + +**Features:** +- ✅ to_dict() for serialization +- ✅ from_dict() for deserialization +- ✅ Nested object support +- ✅ List and dict handling +- ✅ Enum support + +**Example:** +```python +# Convert to dict +spec_dict = to_dict(target_spec) + +# Convert from dict +target_spec = from_dict(spec_dict, TargetSpec) +``` + +### Component Serialization + +**Features:** +- ✅ JSON serialization +- ✅ Component list handling +- ✅ Solution state serialization +- ✅ Deployment spec serialization + +**Example:** +```python +# Serialize components +json_str = serialize_components([comp1, comp2]) + +# Deserialize +components = deserialize_components(json_str) +``` + +--- + +## Error Handling + +Comprehensive error handling and reporting. + +### SymphonyAPIError + +**Features:** +- ✅ Custom exception class +- ✅ HTTP status code capture +- ✅ Response text capture +- ✅ Detailed error messages + +**Example:** +```python +try: + client.register_target("device", spec) +except SymphonyAPIError as e: + print(f"Error: {e}") + print(f"Status: {e.status_code}") + print(f"Details: {e.response_text}") +``` + +### Error Categories + +**Features:** +- ✅ Network errors (connection, timeout) +- ✅ Authentication errors (401) +- ✅ Authorization errors (403) +- ✅ Not found errors (404) +- ✅ Server errors (500+) +- ✅ Validation errors (400) + +### Retry Support + +**Features:** +- ✅ Retry logic patterns in examples +- ✅ Exponential backoff support +- ✅ Configurable retry attempts +- ✅ Retryable error detection + +--- + +## Advanced Features + +### Logging Integration + +**Features:** +- ✅ Custom logger support +- ✅ Request/response logging +- ✅ Debug mode support +- ✅ Configurable log levels + +**Example:** +```python +logger = logging.getLogger("my-app") +client = SymphonyAPI(url, user, pass, logger=logger) +``` + +### Timeout Configuration + +**Features:** +- ✅ Per-client timeout configuration +- ✅ Configurable default timeout +- ✅ Per-request timeout override + +**Example:** +```python +client = SymphonyAPI(url, user, pass, timeout=120.0) +``` + +### Session Management + +**Features:** +- ✅ Persistent HTTP sessions +- ✅ Connection pooling +- ✅ Automatic cleanup +- ✅ Thread safety + +### Content Negotiation + +**Features:** +- ✅ YAML response support +- ✅ JSON response support +- ✅ Plain text support +- ✅ Content-Type handling + +### Advanced Querying + +**Features:** +- ✅ JSONPath support for filtering +- ✅ Document type selection +- ✅ Path-based extraction +- ✅ Query parameters + +**Example:** +```python +# Extract specific field +properties = client.get_target( + "device-001", + doc_type="json", + path="$.spec.properties" +) +``` + +--- + +## Performance Features + +### Efficiency + +- ✅ Connection pooling reduces overhead +- ✅ Token caching minimizes auth requests +- ✅ Minimal dependencies (only `requests`) +- ✅ Efficient JSON parsing +- ✅ Base64 encoding only when needed + +### Scalability + +- ✅ Thread-safe client +- ✅ Support for multiple concurrent requests +- ✅ No global state +- ✅ Resource cleanup + +--- + +## Development Features + +### Testing Support + +- ✅ Comprehensive test suite +- ✅ Mock-friendly design +- ✅ Example unit tests +- ✅ High code coverage + +### Documentation + +- ✅ Detailed docstrings +- ✅ Type hints throughout +- ✅ Usage examples +- ✅ API reference +- ✅ Quick start guide + +### IDE Support + +- ✅ Full type hints for autocomplete +- ✅ Dataclass support +- ✅ Clear error messages +- ✅ Comprehensive documentation strings + +--- + +## Supported Operations + +### Target Operations +✅ Register, ✅ Unregister, ✅ List, ✅ Get, ✅ Ping, ✅ Update Status + +### Solution Operations +✅ Create, ✅ Delete, ✅ List, ✅ Get + +### Instance Operations +✅ Create, ✅ Delete, ✅ List, ✅ Get, ✅ Get Status + +### Deployment Operations +✅ Apply, ✅ Reconcile, ✅ Get Components, ✅ Delete Components + +### Utility Operations +✅ Authenticate, ✅ Health Check, ✅ Get Config + +--- + +## Platform Support + +- ✅ Linux +- ✅ macOS +- ✅ Windows +- ✅ Docker/Containers +- ✅ Kubernetes pods + +## Python Version Support + +- ✅ Python 3.9 +- ✅ Python 3.10 +- ✅ Python 3.11 +- ✅ Python 3.12 +- ✅ Python 3.13 + +--- + +## See Also + +- [Quick Start Guide](QUICKSTART.md) +- [API Reference](API.md) +- [Examples](../examples/) +- [Main README](../README.md) diff --git a/sdks/symphony-python/docs/GLOSSARY.md b/sdks/symphony-python/docs/GLOSSARY.md new file mode 100644 index 000000000..a3440a150 --- /dev/null +++ b/sdks/symphony-python/docs/GLOSSARY.md @@ -0,0 +1,162 @@ +# Symphony Python SDK Glossary + +This glossary defines commonly used terms and concepts in the Symphony Python SDK. + +## Core Resource Concepts + +### Target +A device, edge node, or compute resource that can execute solutions. Examples include IoT devices, edge gateways, or Kubernetes clusters. Targets must be registered with Symphony before deploying instances to them. Managed through lifecycle operations: register, unregister, list, get, and ping. + +### Solution +A deployment specification that defines application components to be deployed. Solutions are YAML-based templates containing component definitions, properties, and metadata. They serve as reusable templates for creating multiple instances. + +### Instance +A concrete deployment of a solution onto specific targets. Instances bind solutions to targets and track deployment status. The actual deployment orchestration happens at the instance level. + +### Component +Individual units within a solution, such as containers, configurations, or scripts. Each component has a name, type, properties, and optional dependencies. Components can be container-based, config-based, or custom types. + +### Deployment +The execution context that ties together solutions, instances, targets, and devices. DeploymentSpec contains the complete state including solution definition, instance configuration, target information, and device assignments. + +## Configuration and Metadata + +### ObjectMeta +Kubernetes-style metadata following standard conventions. Contains name, namespace, labels, and annotations for resource identification and organization. + +### TargetSelector +Specifies which targets a component or instance should be deployed to. Supports name-based selection or label selectors for flexible target matching. + +### ComponentSpec +Detailed specification for a component including name, type, properties, constraints, dependencies, skills, routes, and parameters. + +### SolutionSpec +The specification of a solution containing components, scope, display name, and metadata. + +### InstanceSpec +Specification for an instance deployment including solution reference, target selector, parameters, topologies, pipelines, and version information. + +### Scope +A namespace or partition for organizing Symphony resources. Allows multi-tenancy and resource isolation (e.g., "default" scope). + +## Advanced Deployment Concepts + +### Topology +Defines the physical or logical placement of components on devices within targets. Specifies devices, selectors, and bindings for component distribution across a target's infrastructure. + +### Binding +Specifies how components are bound to providers or roles. Contains role, provider, and configuration information for component lifecycle management. + +### Pipeline +A data processing or orchestration pipeline specification. Can reference skills and parameters for component workflows or data transformations. + +### VersionSpec +Manages solution versioning with percentage-based canary deployments. Supports multi-version deployments with traffic/load distribution percentages. + +### Device +Physical hardware entities within a target. DeviceSpec contains device properties, bindings, and display information. + +## COA (Cloud Object API) Concepts + +### COARequest +HTTP-method request abstraction for provider operations. Contains method (GET/POST/PUT/DELETE), route, content type, body (base64 encoded), metadata, and parameters. + +### COAResponse +Response abstraction with state, content type, body encoding, metadata, and optional redirect URI. Provides factory methods for common responses: `success()`, `error()`, `not_found()`, `bad_request()`. + +### State +Comprehensive enumeration (100+ values) representing operation results. Maps HTTP status codes to operation states, and includes custom Symphony states for configuration errors, async operations, workflow status, and specific provider failures. + +### Content Type +COA bodies support multiple content types: "application/json", "text/plain", "application/octet-stream". Bodies are base64 encoded when transmitted. + +## Status and Progress Tracking + +### SummarySpec +Aggregates deployment results across targets and components. Tracks target counts, success counts, planned deployments, current deployments, and whether all assigned components are deployed. + +### ComponentResultSpec +Tracks individual component deployment status. Contains status (State enum) and message for component-level outcome. + +### TargetResultSpec +Aggregates component results for a target. Contains target-level status string, message, and map of component results. + +### SummaryResult +Complete deployment operation summary including summary specification, state (PENDING/RUNNING/DONE), timestamps, and deployment hash. + +### SummaryState +Enumeration for summary operation state: PENDING (unused), RUNNING (reconciliation in progress), DONE (reconciliation completed). + +## Provider and Configuration Concepts + +### Provider +A backend service that handles specific operations or resources. Referenced by bindings and can include config providers, secret providers, state providers, probe providers, reporters, queue providers, etc. + +### Skill +A named capability that a component possesses. Components can declare skills they implement, which are then referenced by pipelines for orchestration. + +### Route +Specifies routing rules for a component. Includes route pattern, filters, properties, and type for message/request routing between components. + +### Filter +Routing filter specification with direction, parameters, and type for conditional message/request processing. + +### Constraint +Deployment constraints for components or targets. Used to express deployment requirements or restrictions (e.g., affinity, anti-affinity rules). + +## API and Client Concepts + +### SymphonyAPI +The main REST client for Symphony. Manages authentication, session pooling, timeout, and logging. Use as a context manager for automatic cleanup. Supports token-based authentication with automatic refresh. + +### Namespace +API scope for resource organization. Most operations default to `namespace="default"`. Used for multi-tenancy isolation. + +### Doc Type +Response format parameter. Accepts "yaml" or "json". Affects how specs are returned (defaults to YAML for specs). + +### JSONPath +Query parameter for selective field extraction from responses using JSONPath expressions (e.g., `"$.spec.properties"`). + +### Authentication Token +Bearer token returned by `authenticate()` method. Automatically managed and cached by SymphonyAPI client with refresh on expiry. + +## Lifecycle and Operations + +### Reconciliation +The process of ensuring actual state matches desired state. Triggered via `reconcile_solution()` to synchronize deployments with target state. + +### Force Redeploy +A target property indicating components should be redeployed even if already present. Part of TargetSpec configuration. + +### Provisioning Status +Detailed deployment operation status including operation ID, status string, failure cause, errors, and output. Tracks Kubernetes operations, Helm operations, and infrastructure provisioning. + +### Health Check +API connectivity verification. Returns boolean indicating Symphony API accessibility. + +### Ping/Heartbeat +Target keep-alive mechanism. Targets send periodic pings to indicate they're active and responsive. + +### Opt-out Reconciliation +InstanceSpec property allowing instances to skip automatic reconciliation when enabled. + +## Common Symphony Workflows + +### Register Target +Initial operation to register a device or edge node with Symphony. Must be done before deploying instances to a target. + +### Create Solution +Define a template of components and resources. Solutions are reusable across multiple instances. + +### Create Instance +Instantiate a solution on specific target(s). Triggers deployment orchestration. + +### Monitor Status +Track instance and deployment progress via `get_instance_status()` and SummaryResult tracking. + +### Reconcile +Force synchronization between desired (instance spec) and actual (deployed) state on targets. + +### Cleanup +Remove instances, solutions, and unregister targets. Supports direct delete option to bypass cleanup pipelines. diff --git a/sdks/symphony-python/docs/QUICKSTART.md b/sdks/symphony-python/docs/QUICKSTART.md new file mode 100644 index 000000000..e8b6d3a46 --- /dev/null +++ b/sdks/symphony-python/docs/QUICKSTART.md @@ -0,0 +1,375 @@ +# Symphony Python SDK - Quick Start Guide + +Get started with the Symphony Python SDK in minutes. + +## Installation + +```bash +pip install symphony-sdk +``` + +Or install from source: + +```bash +git clone https://github.com/eclipse-symphony/symphony.git +cd symphony/sdks/symphony-python +pip install -e . +``` + +## Basic Usage + +### 1. Connect to Symphony + +```python +from symphony_sdk import SymphonyAPI + +# Initialize the client +client = SymphonyAPI( + base_url="https://symphony.example.com", + username="your-username", + password="your-password" +) + +# Use context manager (recommended) +with SymphonyAPI(base_url, username, password) as client: + # Your code here + pass +``` + +### 2. Work with Targets + +Targets represent devices, edge nodes, or compute resources. + +```python +# Register a new target +target_spec = { + "displayName": "Edge Gateway 001", + "scope": "default", + "properties": { + "location": "datacenter-1", + "os": "linux", + "arch": "amd64" + } +} + +client.register_target("gateway-001", target_spec) + +# List all targets +targets = client.list_targets() +for target in targets.get('items', []): + print(target['metadata']['name']) + +# Get target details +target = client.get_target("gateway-001") + +# Send heartbeat +client.ping_target("gateway-001") + +# Unregister target +client.unregister_target("gateway-001") +``` + +### 3. Deploy Solutions + +Solutions define application components to be deployed. + +```python +import yaml + +# Define a solution +solution = { + "displayName": "Web Application", + "scope": "default", + "components": [ + { + "name": "nginx", + "type": "container", + "properties": { + "container.image": "nginx:latest", + "container.ports": "80:8080" + } + } + ] +} + +# Create the solution +solution_yaml = yaml.dump(solution) +client.create_solution("web-app", solution_yaml) + +# Create an instance (deployment) +instance_spec = { + "solution": "web-app", + "target": {"name": "gateway-001"}, + "scope": "default", + "displayName": "Production Web App" +} + +client.create_instance("web-app-prod", instance_spec) + +# Check deployment status +status = client.get_instance_status("web-app-prod") +print(f"Status: {status.get('status')}") +``` + +### 4. Use COA (Cloud Object API) + +COA provides a unified interface for provider operations. + +```python +from symphony_sdk import COARequest, COAResponse, State + +# Create a request +request = COARequest( + method="GET", + route="/components/list", + content_type="application/json" +) +request.set_body({"filter": "active"}) + +# Create a response +response = COAResponse.success({ + "components": ["nginx", "redis"] +}) + +# Handle errors +error_response = COAResponse.error( + "Component not found", + state=State.NOT_FOUND +) +``` + +### 5. Track Deployment Status + +```python +from symphony_sdk import ( + SummaryResult, SummarySpec, SummaryState, + create_success_component_result, + create_target_result +) + +# Create a deployment summary +summary = SummarySpec( + target_count=1, + success_count=1, + planned_deployment=2, + current_deployed=2, + all_assigned_deployed=True +) + +# Add target results +target_result = create_target_result( + status="OK", + message="All components deployed", + component_results={ + "nginx": create_success_component_result("Deployed"), + "redis": create_success_component_result("Deployed") + } +) +summary.update_target_result("gateway-001", target_result) + +# Create summary result +result = SummaryResult( + summary=summary, + state=SummaryState.DONE +) + +if result.is_deployment_finished(): + print("Deployment complete!") +``` + +## Common Patterns + +### Error Handling + +```python +from symphony_sdk import SymphonyAPIError + +try: + client.register_target("device-001", spec) +except SymphonyAPIError as e: + print(f"API Error: {e}") + if e.status_code: + print(f"Status Code: {e.status_code}") + if e.response_text: + print(f"Details: {e.response_text}") +``` + +### Polling for Completion + +```python +import time + +def wait_for_deployment(client, instance_name, timeout=300): + """Wait for deployment to complete.""" + start_time = time.time() + + while time.time() - start_time < timeout: + try: + status = client.get_instance_status(instance_name) + state = status.get('status', 'Unknown') + + if state == 'Succeeded': + return True + elif state == 'Failed': + return False + + time.sleep(5) # Check every 5 seconds + except SymphonyAPIError: + time.sleep(5) + + return False # Timeout + +# Use it +if wait_for_deployment(client, "web-app-prod"): + print("Deployment succeeded!") +else: + print("Deployment failed or timed out") +``` + +### Using Dataclasses for Type Safety + +```python +from symphony_sdk import ( + TargetSpec, ComponentSpec, TargetState, + ObjectMeta, InstanceSpec, TargetSelector +) + +# Create target with dataclasses +metadata = ObjectMeta( + name="gateway-001", + namespace="default", + labels={"env": "prod"} +) + +component = ComponentSpec( + name="nginx", + type="container", + properties={"image": "nginx:latest"} +) + +target_spec = TargetSpec( + displayName="Edge Gateway", + components=[component], + properties={"location": "dc1"} +) + +target = TargetState( + metadata=metadata, + spec=target_spec +) + +# Create instance with dataclasses +instance = InstanceSpec( + name="my-app", + solution="web-app", + target=TargetSelector(name="gateway-001"), + parameters={"replicas": "3"} +) +``` + +### Batch Operations + +```python +# Register multiple targets +targets = [ + ("device-001", {"displayName": "Device 1", ...}), + ("device-002", {"displayName": "Device 2", ...}), + ("device-003", {"displayName": "Device 3", ...}), +] + +for name, spec in targets: + try: + client.register_target(name, spec) + print(f"✓ Registered {name}") + except SymphonyAPIError as e: + print(f"✗ Failed to register {name}: {e}") +``` + +## Configuration + +### Custom Timeout + +```python +# Set custom timeout (default is 30 seconds) +client = SymphonyAPI( + base_url=base_url, + username=username, + password=password, + timeout=60.0 # 60 seconds +) +``` + +### Custom Logger + +```python +import logging + +# Create custom logger +logger = logging.getLogger("my-app") +logger.setLevel(logging.DEBUG) +handler = logging.StreamHandler() +handler.setFormatter(logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' +)) +logger.addHandler(handler) + +# Use with client +client = SymphonyAPI( + base_url=base_url, + username=username, + password=password, + logger=logger +) +``` + +## Next Steps + +- Explore the [examples directory](../examples/) for more detailed examples +- Read the [API Reference](API.md) for complete API documentation +- Check the [Symphony documentation](https://github.com/eclipse-symphony/symphony) for architecture concepts +- Review the [CHANGELOG](../CHANGELOG.md) for version history + +## Common Issues + +### Authentication Errors + +```python +# Ensure credentials are correct +try: + token = client.authenticate() + print("Authentication successful!") +except SymphonyAPIError as e: + if e.status_code == 401: + print("Invalid credentials") + else: + print(f"Auth error: {e}") +``` + +### Connection Errors + +```python +# Check if Symphony is reachable +if client.health_check(): + print("Connected to Symphony") +else: + print("Cannot reach Symphony API") + print(f"URL: {client.base_url}") +``` + +### Timeout Issues + +```python +# Increase timeout for slow operations +client = SymphonyAPI( + base_url=base_url, + username=username, + password=password, + timeout=120.0 # 2 minutes +) +``` + +## Support + +- Report issues on [GitHub](https://github.com/eclipse-symphony/symphony/issues) +- Check the [main documentation](../README.md) +- Review [test files](../tests/) for more examples diff --git a/sdks/symphony-python/examples/01_basic_client.py b/sdks/symphony-python/examples/01_basic_client.py new file mode 100644 index 000000000..4d9e0ad24 --- /dev/null +++ b/sdks/symphony-python/examples/01_basic_client.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Basic Symphony API Client Usage Example + +This example demonstrates how to: +1. Initialize the Symphony API client +2. Authenticate with the Symphony API +3. Perform basic health checks +4. Use the client as a context manager +""" + +from symphony_sdk import SymphonyAPI + + +def main(): + # Initialize the Symphony API client + # Replace with your actual Symphony instance URL and credentials + base_url = "http://localhost:8082/v1alpha2" + username = "admin" + password = "" + + # Method 1: Using context manager (recommended) + # The context manager ensures the session is properly closed + print("Example 1: Using context manager") + with SymphonyAPI(base_url, username, password) as client: + # Authentication happens automatically when needed + print(f"Connected to: {client.base_url}") + + # Perform a health check + if client.health_check(): + print("✓ Symphony API is healthy") + else: + print("✗ Symphony API health check failed") + + print("\n" + "=" * 60 + "\n") + + # Method 2: Manual session management + print("Example 2: Manual session management") + client = SymphonyAPI(base_url=base_url, username=username, password=password, timeout=10.0) + + try: + # Explicitly authenticate (Optional. Auth happens automatically) + token = client.authenticate() + print("✓ Authenticated successfully") + print(f" Token (first 20 chars): {token[:20]}...") + except Exception as e: + print(f"✗ Authentication failed: {e}") + finally: + # Close the client when done + client.close() + print("✓ Client closed") + return True + + +if __name__ == "__main__": + print("=" * 60) + print("Symphony SDK - Basic Client Usage") + print("=" * 60 + "\n") + + print("NOTE: Update the credentials in this script before running!\n") + + # Uncomment the line below after updating credentials + # main() + + print("Update base_url, username, and password in the script,") + print("then uncomment the main() call to run this example.") diff --git a/sdks/symphony-python/examples/02_target_management.py b/sdks/symphony-python/examples/02_target_management.py new file mode 100644 index 000000000..e01cd3bae --- /dev/null +++ b/sdks/symphony-python/examples/02_target_management.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""Target Management Example + +This example demonstrates how to: +1. Register a new target +2. List all targets +3. Get target details +4. Update target status +5. Ping a target +6. Unregister a target +""" + +from symphony_sdk import SymphonyAPI, SymphonyAPIError + + +def create_target_spec(): + """Create a sample target specification.""" + return { + "displayName": "Example IoT Device", + "scope": "default", + "properties": { + "location": "datacenter-1", + "os": "linux", + "arch": "amd64", + "device-type": "edge-gateway", + }, + "components": [], + "metadata": {"owner": "infrastructure-team", "environment": "production"}, + } + + +def main(): + # Initialize client + base_url = "http://localhost:8082/v1alpha2" + username = "admin" + password = "" + + with SymphonyAPI(base_url, username, password) as client: + target_name = "example-device-001" + + try: + # 1. Register a new target + print(f"1. Registering target '{target_name}'...") + target_spec = create_target_spec() + result = client.register_target(target_name, target_spec) + print(" ✓ Target registered successfully") + + # 2. List all targets + print("\n2. Listing all targets...") + targets = client.list_targets() + print(f" ✓ Found {len(targets)} targets") + for target in targets[:5]: # Show first 5 + print(f" - {target.get('metadata', {}).get('name', 'unknown')}") + + # 3. Get specific target details + print(f"\n3. Getting details for '{target_name}'...") + target_details = client.get_target(target_name) + print(" ✓ Target details retrieved", target_details) + + # 4. Ping the target (heartbeat) + print(f"\n4. Sending heartbeat to '{target_name}'...") + ping_result = client.ping_target(target_name) + print(f" ✓ Heartbeat sent successfully - Response: {ping_result}") + + # 5. Update target status + print("\n5. Updating target status...") + status_data = { + "properties": {"health": "healthy", "last_check": "2024-01-01T00:00:00Z"} + } + client.update_target_status(target_name, status_data) + print(" ✓ Target status updated") + + # 6. Unregister the target + print(f"\n6. Unregistering target '{target_name}'...") + client.unregister_target(target_name) + print(" ✓ Target unregistered successfully") + + except SymphonyAPIError as e: + print(f"\n✗ Error: {e}") + if e.status_code: + print(f" Status code: {e.status_code}") + if e.response_text: + print(f" Response: {e.response_text[:200]}") + + +def example_using_dataclasses(): + """Example using Symphony SDK dataclasses.""" + from symphony_sdk import ComponentSpec, ObjectMeta, TargetSpec, TargetState + + print("\n" + "=" * 60) + print("Using Symphony SDK Dataclasses") + print("=" * 60 + "\n") + + # Create target spec using dataclasses + metadata = ObjectMeta( + name="example-device-002", + namespace="default", + labels={"env": "prod", "region": "us-west"}, + annotations={"description": "Production edge gateway"}, + ) + + # Create component specs + component = ComponentSpec( + name="web-app", type="container", properties={"image": "nginx:latest", "port": "80"} + ) + + # Create target spec + target_spec = TargetSpec( + displayName="Example Device 002", + scope="default", + properties={"location": "datacenter-2", "os": "linux"}, + components=[component], + metadata={"owner": "dev-team"}, + ) + + # Create target state + target_state = TargetState(metadata=metadata, spec=target_spec) + + print("✓ Created target state with dataclasses") + print(f" Name: {target_state.metadata.name}") + print(f" Components: {len(target_state.spec.components)}") + print(f" First component: {target_state.spec.components[0].name}") + + +if __name__ == "__main__": + print("=" * 60) + print("Symphony SDK - Target Management") + print("=" * 60 + "\n") + + print("NOTE: Update the credentials in this script before running!\n") + + # Uncomment the lines below after updating credentials + # main() + # example_using_dataclasses() + + print("Update base_url, username, and password in the script,") + print("then uncomment the main() and example_using_dataclasses() call to run this example.") diff --git a/sdks/symphony-python/examples/03_solution_deployment.py b/sdks/symphony-python/examples/03_solution_deployment.py new file mode 100644 index 000000000..c6d453b1b --- /dev/null +++ b/sdks/symphony-python/examples/03_solution_deployment.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +"""Solution and Instance Management Example + +This example demonstrates how to: +1. Create a solution +2. Create an instance from a solution +3. Apply a deployment +4. Check deployment status +5. List solutions and instances +6. Clean up resources +""" + +import time + +import yaml + +from symphony_sdk import SymphonyAPI, SymphonyAPIError + + +def create_solution_yaml(): + """Create a sample solution specification in YAML format.""" + solution = { + "displayName": "Web Application Stack", + "rootResource": "web-app-stack", + "metadata": {"version": "1.0.0", "description": "A simple web application with nginx"}, + "components": [ + { + "name": "nginx-server", + "type": "container", + "properties": {"container.image": "nginx:1.21", "container.ports": "80:8080"}, + "metadata": {"description": "Nginx web server"}, + }, + { + "name": "app-config", + "type": "config", + "properties": { + "config.type": "configmap", + "config.data": "server_name=example.com", + }, + }, + ], + } + return yaml.dump(solution) + + +def main(): + base_url = "http://localhost:8082/v1alpha2" + username = "admin" + password = "" + + solution_name = "web-app-stack-v-1.0.0" # Format: -v- + instance_name = "web-app-prod" + + with SymphonyAPI(base_url, username, password) as client: + try: + # 1. Create a solution + print("1. Creating solution...") + solution_yaml = create_solution_yaml() + client.create_solution(solution_name, solution_yaml) + print(f" ✓ Solution '{solution_name}' created") + + # 2. Verify solution was created + print("\n2. Verifying solution...") + solution = client.get_solution(solution_name) + print(" ✓ Solution retrieved") + spec = solution.get("spec", {}) + print(f" Display Name: {spec.get('displayName', 'N/A')}") + print(f" Components: {len(spec.get('components', []))}") + + # 3. Create an instance from the solution + print(f"\n3. Creating instance '{instance_name}'...") + instance_spec = { + "solution": solution_name, + "target": { + "name": "example-device-001" # Target must exist + }, + "displayName": "Production Web App", + "parameters": {"environment": "production", "replicas": "3"}, + } + client.create_instance(instance_name, instance_spec) + print(f" ✓ Instance '{instance_name}' created") + + # 4. Check instance status + print("\n4. Checking instance status...") + for attempt in range(5): + try: + status = client.get_instance_status(instance_name) + print(f" Attempt {attempt + 1}/5:") + print(f" Status: {status}") + + # Check if deployment is complete + if status.get("status") == "Succeeded": + print(" ✓ Deployment completed successfully!") + break + elif status.get("status") == "Failed": + print(" ✗ Deployment failed!") + break + + time.sleep(2) # Wait before checking again + except Exception as e: + print(f" Status check error: {e}") + break + + # 5. List all solutions + print("\n5. Listing all solutions...") + solutions = client.list_solutions() + solutions_list = ( + solutions if isinstance(solutions, list) else solutions.get("items", []) + ) + print(f" ✓ Found {len(solutions_list)} solutions") + for sol in solutions_list[:5]: + print(f" - {sol.get('metadata', {}).get('name', 'unknown')}") + + # 6. List all instances + print("\n6. Listing all instances...") + instances = client.list_instances() + instances_list = ( + instances if isinstance(instances, list) else instances.get("items", []) + ) + print(f" ✓ Found {len(instances_list)} instances") + for inst in instances_list[:5]: + print(f" - {inst.get('metadata', {}).get('name', 'unknown')}") + + # 7. Clean up - delete instance and solution + print("\n7. Cleaning up resources...") + print(f" Deleting instance '{instance_name}'...") + client.delete_instance(instance_name) + print(" ✓ Instance deleted") + + print(f" Deleting solution '{solution_name}'...") + client.delete_solution(solution_name) + print(" ✓ Solution deleted") + + except SymphonyAPIError as e: + print(f"\n✗ Error: {e}") + if e.status_code: + print(f" Status code: {e.status_code}") + + +def example_using_instance_spec_dataclass(): + """Example using InstanceSpec dataclass.""" + from symphony_sdk import InstanceSpec, TargetSelector + + print("\n" + "=" * 60) + print("Using InstanceSpec Dataclass") + print("=" * 60 + "\n") + + # Create instance spec using dataclass + instance = InstanceSpec( + name="my-instance", + solution="my-solution", + target=TargetSelector(name="my-target", selector={"location": "datacenter-1"}), + scope="default", + display_name="My Application Instance", + parameters={"replicas": "3", "memory": "2Gi"}, + metadata={"owner": "platform-team"}, + ) + + print("✓ Created InstanceSpec with dataclass") + print(f" Name: {instance.name}") + print(f" Solution: {instance.solution}") + print(f" Target: {instance.target.name}") + print(f" Parameters: {instance.parameters}") + + +if __name__ == "__main__": + print("=" * 60) + print("Symphony SDK - Solution and Instance Management") + print("=" * 60 + "\n") + + print("NOTE: Update the credentials in this script before running!\n") + + # Uncomment the lines below after updating credentials + # main() + # example_using_instance_spec_dataclass() + + print("Update base_url, username, and password in the script,") + print("then uncomment the main() call to run this example.") diff --git a/sdks/symphony-python/examples/04_coa_provider.py b/sdks/symphony-python/examples/04_coa_provider.py new file mode 100644 index 000000000..54771bb48 --- /dev/null +++ b/sdks/symphony-python/examples/04_coa_provider.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +"""COA (Cloud Object API) Provider Example + +This example demonstrates how to: +1. Create COA requests and responses +2. Handle different content types (JSON, text, binary) +3. Work with COA body encoding/decoding +4. Build a simple provider that responds to COA requests +""" + +import json + +from symphony_sdk import ( + COARequest, + COAResponse, + State, + deserialize_coa_request, + serialize_coa_request, +) + + +def example_json_request(): + """Example: Creating and handling JSON COA requests.""" + print("1. JSON COA Request Example") + print("-" * 40) + + # Create a COA request with JSON body + request = COARequest( + method="GET", + route="/components/list", + content_type="application/json", + parameters={"namespace": "default"}, + metadata={"request-id": "123"}, + ) + + # Set JSON body data + request.set_body({"filter": "active", "limit": 10}) + + print("✓ Created COA request") + print(f" Method: {request.method}") + print(f" Route: {request.route}") + print(f" Content-Type: {request.content_type}") + + # Serialize to JSON string + request_json = serialize_coa_request(request) + print("\n✓ Serialized request:") + print(f" {request_json[:150]}...") + + # Deserialize back + restored_request = deserialize_coa_request(request_json) + restored_body = restored_request.get_body() + print("\n✓ Deserialized and decoded body:") + print(f" {restored_body}") + + +def example_json_response(): + """Example: Creating JSON COA responses.""" + print("\n2. JSON COA Response Example") + print("-" * 40) + + # Create a success response + response = COAResponse.success( + data={ + "components": [ + {"name": "web-server", "status": "running"}, + {"name": "database", "status": "running"}, + ], + "total": 2, + } + ) + + print("✓ Created success response") + print(f" State: {response.state} ({response.state.name})") + + # Get body data + body = response.get_body() + print(f" Body: {body}") + + # Create error responses + error_response = COAResponse.error("Component not found", state=State.NOT_FOUND) + print("\n✓ Created error response") + print(f" State: {error_response.state} ({error_response.state.name})") + print(f" Body: {error_response.get_body()}") + + # Create bad request response + bad_request = COAResponse.bad_request("Invalid component name format") + print("\n✓ Created bad request response") + print(f" State: {bad_request.state}") + + +def example_text_content(): + """Example: Working with plain text content.""" + print("\n3. Plain Text Content Example") + print("-" * 40) + + # Create request with text content + request = COARequest(method="POST", route="/logs/append", content_type="text/plain") + + log_message = "Application started successfully at 2024-01-01 10:00:00" + request.set_body(log_message) + + print("✓ Created text request") + print(f" Original text: {log_message}") + print(f" Stored body: {request.body[:50]}...") + + # Retrieve text + retrieved_text = request.get_body() + print(f" Retrieved text: {retrieved_text}") + + # Create text response + response = COAResponse(content_type="text/plain", state=State.OK) + response.set_body("Log entry appended successfully") + + print("\n✓ Created text response") + print(f" Response: {response.get_body()}") + + +def example_binary_content(): + """Example: Working with binary content.""" + print("\n4. Binary Content Example") + print("-" * 40) + + # Create request with binary content + request = COARequest( + method="POST", route="/files/upload", content_type="application/octet-stream" + ) + + # Simulate binary data + binary_data = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR" + request.set_body(binary_data) + + print("✓ Created binary request") + print(f" Original bytes: {binary_data[:20]}") + print(f" Stored body (base64): {request.body[:50]}...") + + # Retrieve binary data + retrieved_binary = request.get_body() + print(f" Retrieved bytes: {retrieved_binary[:20]}") + print(f" Match: {binary_data == retrieved_binary}") + + +def example_provider_simulation(): + """Example: Simulating a COA provider.""" + print("\n5. Provider Simulation Example") + print("-" * 40) + + def handle_component_get(request: COARequest) -> COAResponse: + """Handle component GET requests.""" + body = request.get_body() + component_name = body.get("name", "") + + if not component_name: + return COAResponse.bad_request("Component name is required") + + # Simulate component lookup + if component_name == "web-server": + return COAResponse.success( + { + "name": "web-server", + "status": "running", + "properties": {"image": "nginx:latest", "port": "80"}, + } + ) + else: + return COAResponse.not_found(f"Component '{component_name}' not found") + + # Simulate incoming request + incoming = COARequest(method="GET", route="/components/get", content_type="application/json") + incoming.set_body({"name": "web-server"}) + + print("✓ Received request") + print(f" Route: {incoming.route}") + print(f" Body: {incoming.get_body()}") + + # Handle request + response = handle_component_get(incoming) + + print("\n✓ Generated response") + print(f" State: {response.state} ({response.state.name})") + print(f" Body: {json.dumps(response.get_body(), indent=2)}") + + # Test with missing component + print("\n" + "-" * 40) + incoming2 = COARequest(method="GET", route="/components/get") + incoming2.set_body({"name": "unknown-component"}) + + response2 = handle_component_get(incoming2) + print("✓ Response for missing component:") + print(f" State: {response2.state} ({response2.state.name})") + print(f" Body: {response2.get_body()}") + + +def example_deployment_spec(): + """Example: Working with DeploymentSpec.""" + print("\n6. DeploymentSpec Example") + print("-" * 40) + + from symphony_sdk import ( + ComponentSpec, + DeploymentSpec, + ObjectMeta, + SolutionSpec, + SolutionState, + to_dict, + ) + + # Create a deployment spec + component1 = ComponentSpec( + name="frontend", type="container", properties={"image": "nginx:latest"} + ) + + component2 = ComponentSpec(name="backend", type="container", properties={"image": "node:18"}) + + solution = SolutionState( + metadata=ObjectMeta(name="my-app"), + spec=SolutionSpec(components=[component1, component2], displayName="My Application"), + ) + + deployment = DeploymentSpec(solutionName="my-app", solution=solution, activeTarget="target-001") + + print("✓ Created deployment spec") + print(f" Solution: {deployment.solutionName}") + print(f" Components: {len(deployment.solution.spec.components)}") + + # Get component slice + components = deployment.get_components_slice() + print("\n✓ Component slice:") + for comp in components: + print(f" - {comp.name} ({comp.type})") + + # Convert to dictionary + deployment_dict = to_dict(deployment) + print(f"\n✓ Converted to dict (keys): {list(deployment_dict.keys())}") + + +if __name__ == "__main__": + print("=" * 60) + print("Symphony SDK - COA Provider Examples") + print("=" * 60 + "\n") + + example_json_request() + example_json_response() + example_text_content() + example_binary_content() + example_provider_simulation() + example_deployment_spec() + + print("\n" + "=" * 60) + print("All examples completed successfully!") + print("=" * 60) diff --git a/sdks/symphony-python/examples/05_summary_tracking.py b/sdks/symphony-python/examples/05_summary_tracking.py new file mode 100644 index 000000000..a57ea09ab --- /dev/null +++ b/sdks/symphony-python/examples/05_summary_tracking.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python3 +"""Summary and Status Tracking Example + +This example demonstrates how to: +1. Create summary results for deployments +2. Track component deployment status +3. Generate status messages +4. Handle deployment completion +""" + +from symphony_sdk import ( + State, + SummaryResult, + SummarySpec, + SummaryState, + create_failed_component_result, + create_success_component_result, + create_target_result, +) + + +def example_successful_deployment(): + """Example: Tracking a successful deployment.""" + print("1. Successful Deployment Example") + print("-" * 40) + + # Create summary spec + summary = SummarySpec( + target_count=2, + success_count=2, + planned_deployment=4, + current_deployed=4, + all_assigned_deployed=True, + ) + + # Add successful target results + target1_result = create_target_result( + status="OK", + message="All components deployed successfully", + component_results={ + "web-server": create_success_component_result("Deployed"), + "database": create_success_component_result("Deployed"), + }, + ) + + target2_result = create_target_result( + status="OK", + message="All components deployed successfully", + component_results={ + "cache": create_success_component_result("Deployed"), + "worker": create_success_component_result("Deployed"), + }, + ) + + summary.update_target_result("target-1", target1_result) + summary.update_target_result("target-2", target2_result) + + # Create summary result + result = SummaryResult( + summary=summary, + summary_id="deployment-001", + generation="v1", + state=SummaryState.DONE, + deployment_hash="abc123", + ) + + print("✓ Deployment completed") + print(f" State: {result.state.name}") + print(f" Targets: {result.summary.target_count}") + print(f" Success: {result.summary.success_count}") + print(f" Deployed: {result.summary.current_deployed}/{result.summary.planned_deployment}") + print(f" All deployed: {result.summary.all_assigned_deployed}") + + if result.is_deployment_finished(): + print("\n✓ Deployment is finished!") + + # Show target details + print("\n✓ Target results:") + for target_name, target_result in result.summary.target_results.items(): + print(f"\n {target_name}:") + print(f" Status: {target_result.status}") + print(f" Message: {target_result.message}") + print(" Components:") + for comp_name, comp_result in target_result.component_results.items(): + print(f" - {comp_name}: {comp_result.status.name} ({comp_result.message})") + + +def example_failed_deployment(): + """Example: Tracking a failed deployment.""" + print("\n2. Failed Deployment Example") + print("-" * 40) + + # Create summary spec for failed deployment + summary = SummarySpec( + target_count=2, + success_count=1, # Only 1 target succeeded + planned_deployment=4, + current_deployed=2, # Only 2 components deployed + all_assigned_deployed=False, + summary_message="Some components failed to deploy", + ) + + # Target 1: Success + target1_result = create_target_result( + status="OK", + message="All components deployed", + component_results={ + "web-server": create_success_component_result("Deployed"), + "database": create_success_component_result("Deployed"), + }, + ) + + # Target 2: Failure + target2_result = create_target_result( + status="Failed", + message="Component deployment failed", + component_results={ + "cache": create_failed_component_result( + "Image pull failed: connection timeout", State.INTERNAL_ERROR + ), + "worker": create_failed_component_result( + "Insufficient resources", State.INTERNAL_ERROR + ), + }, + ) + + summary.update_target_result("target-1", target1_result) + summary.update_target_result("target-2", target2_result) + + # Create summary result + result = SummaryResult(summary=summary, summary_id="deployment-002", state=SummaryState.DONE) + + print("✓ Deployment completed with errors") + print(f" State: {result.state.name}") + print(f" Targets: {result.summary.target_count}") + print(f" Success: {result.summary.success_count}") + print(f" Deployed: {result.summary.current_deployed}/{result.summary.planned_deployment}") + + # Generate detailed status message + status_message = result.summary.generate_status_message() + print("\n✗ Status Message:") + print(f" {status_message}") + + # Show failed components + print("\n✗ Failed components:") + for target_name, target_result in result.summary.target_results.items(): + if target_result.status != "OK": + print(f"\n {target_name}:") + for comp_name, comp_result in target_result.component_results.items(): + if comp_result.status != State.OK: + print(f" - {comp_name}: {comp_result.message}") + + +def example_incremental_updates(): + """Example: Incrementally updating deployment status.""" + print("\n3. Incremental Status Updates Example") + print("-" * 40) + + # Initialize summary + summary = SummarySpec(target_count=1, planned_deployment=3) + + result = SummaryResult( + summary=summary, + summary_id="deployment-003", + state=SummaryState.RUNNING, # Still in progress + ) + + print("✓ Deployment started") + print(f" State: {result.state.name}") + + # Update 1: First component deployed + print("\n Update 1: web-server deployed") + target_result = create_target_result( + status="InProgress", + component_results={"web-server": create_success_component_result("Deployed")}, + ) + result.summary.update_target_result("target-1", target_result) + result.summary.current_deployed = 1 + print(f" Deployed: {result.summary.current_deployed}/{result.summary.planned_deployment}") + + # Update 2: Second component deployed + print("\n Update 2: database deployed") + target_result = create_target_result( + status="InProgress", + component_results={"database": create_success_component_result("Deployed")}, + ) + result.summary.update_target_result("target-1", target_result) + result.summary.current_deployed = 2 + print(f" Deployed: {result.summary.current_deployed}/{result.summary.planned_deployment}") + + # Update 3: Third component deployed - deployment complete + print("\n Update 3: cache deployed") + target_result = create_target_result( + status="OK", + message="All components deployed", + component_results={"cache": create_success_component_result("Deployed")}, + ) + result.summary.update_target_result("target-1", target_result) + result.summary.current_deployed = 3 + result.summary.success_count = 1 + result.summary.all_assigned_deployed = True + result.state = SummaryState.DONE + print(f" Deployed: {result.summary.current_deployed}/{result.summary.planned_deployment}") + + print("\n✓ Deployment completed!") + print(f" Final state: {result.state.name}") + print(f" All deployed: {result.summary.all_assigned_deployed}") + + +def example_serialization(): + """Example: Serializing and deserializing summary results.""" + print("\n4. Serialization Example") + print("-" * 40) + + # Create a summary result + summary = SummarySpec( + target_count=1, + success_count=1, + planned_deployment=2, + current_deployed=2, + all_assigned_deployed=True, + ) + + target_result = create_target_result( + status="OK", component_results={"app": create_success_component_result("Deployed")} + ) + summary.update_target_result("target-1", target_result) + + result = SummaryResult(summary=summary, summary_id="deployment-004", state=SummaryState.DONE) + + print("✓ Created summary result") + + # Convert to dictionary + result_dict = result.to_dict() + print("\n✓ Converted to dictionary:") + print(f" Keys: {list(result_dict.keys())}") + print(f" Summary ID: {result_dict['summaryid']}") + print(f" State: {result_dict['state']}") + + # Restore from dictionary + restored = SummaryResult.from_dict(result_dict) + print("\n✓ Restored from dictionary:") + print(f" Summary ID: {restored.summary_id}") + print(f" State: {restored.state.name}") + print(f" Target count: {restored.summary.target_count}") + print(f" All deployed: {restored.summary.all_assigned_deployed}") + + +def example_removal_operation(): + """Example: Tracking a removal/uninstall operation.""" + print("\n5. Removal Operation Example") + print("-" * 40) + + summary = SummarySpec( + target_count=1, + success_count=1, + is_removal=True, # This is a removal operation + removed=True, + ) + + target_result = create_target_result( + status="OK", + message="Components removed successfully", + component_results={ + "web-server": create_success_component_result("Removed"), + "database": create_success_component_result("Removed"), + }, + ) + summary.update_target_result("target-1", target_result) + + result = SummaryResult(summary=summary, state=SummaryState.DONE) + + print("✓ Removal operation completed") + print(f" Is removal: {result.summary.is_removal}") + print(f" Removed: {result.summary.removed}") + print(f" Target count: {result.summary.target_count}") + print(f" Success count: {result.summary.success_count}") + + +if __name__ == "__main__": + print("=" * 60) + print("Symphony SDK - Summary and Status Tracking") + print("=" * 60 + "\n") + + example_successful_deployment() + example_failed_deployment() + example_incremental_updates() + example_serialization() + example_removal_operation() + + print("\n" + "=" * 60) + print("All examples completed successfully!") + print("=" * 60) diff --git a/sdks/symphony-python/examples/06_error_handling.py b/sdks/symphony-python/examples/06_error_handling.py new file mode 100644 index 000000000..f15ab2253 --- /dev/null +++ b/sdks/symphony-python/examples/06_error_handling.py @@ -0,0 +1,390 @@ +#!/usr/bin/env python3 +"""Error Handling and Best Practices Example + +This example demonstrates: +1. Proper error handling with SymphonyAPIError +2. Retry logic for transient failures +3. Logging and debugging +4. Timeout handling +5. Validation best practices +""" + +import logging +import time +from typing import Any, Dict, Optional + +from symphony_sdk import SymphonyAPI, SymphonyAPIError + +# Configure logging +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + + +def example_basic_error_handling(): + """Example: Basic error handling.""" + print("1. Basic Error Handling") + print("-" * 40) + + base_url = "http://localhost:8082/v1alpha2" + username = "admin" + password = "" + + try: + with SymphonyAPI(base_url, username, password) as client: + # Attempt to get a non-existent target + target = client.get_target("non-existent-target") + + except SymphonyAPIError as e: + print("✗ Caught SymphonyAPIError") + print(f" Message: {e}") + print(f" Status Code: {e.status_code}") + + # Handle specific HTTP status codes + if e.status_code == 404: + print(" → Target not found") + elif e.status_code == 401: + print(" → Authentication failed - check credentials") + elif e.status_code == 403: + print(" → Permission denied") + elif e.status_code == 500: + print(" → Server error - try again later") + else: + print(f" → Unexpected error: {e.status_code}") + + # Response text may contain additional details + if e.response_text: + print(f" Response details: {e.response_text[:200]}") + + +def retry_with_backoff( + func, + max_retries: int = 3, + initial_delay: float = 1.0, + backoff_factor: float = 2.0, + retryable_codes: tuple = (500, 502, 503, 504), +): + """Retry a function with exponential backoff. + + Args: + func: Function to retry + max_retries: Maximum number of retry attempts + initial_delay: Initial delay between retries in seconds + backoff_factor: Multiplier for delay after each retry + retryable_codes: HTTP status codes that should trigger retry + """ + delay = initial_delay + last_exception = None + + for attempt in range(max_retries + 1): + try: + return func() + except SymphonyAPIError as e: + last_exception = e + + # Don't retry client errors (4xx) except for specific cases + if e.status_code and 400 <= e.status_code < 500: + if e.status_code not in (408, 429): # Timeout, Too Many Requests + raise + + # Don't retry if not a retryable server error + if e.status_code and e.status_code not in retryable_codes: + raise + + if attempt < max_retries: + logger.warning(f"Attempt {attempt + 1} failed: {e}. Retrying in {delay}s...") + time.sleep(delay) + delay *= backoff_factor + else: + logger.error(f"All {max_retries} retry attempts failed") + + raise last_exception + + +def example_retry_logic(): + """Example: Implementing retry logic.""" + print("\n2. Retry Logic with Exponential Backoff") + print("-" * 40) + + base_url = "http://localhost:8082/v1alpha2" + username = "admin" + password = "" + + with SymphonyAPI(base_url, username, password) as client: + + def create_target(): + """Function to retry.""" + return client.register_target( + "my-target", {"displayName": "My Target", "properties": {"location": "dc1"}} + ) + + try: + # Retry with exponential backoff + result = retry_with_backoff(create_target, max_retries=3, initial_delay=1.0) + print("✓ Target created successfully (possibly after retries)") + + except SymphonyAPIError as e: + print(f"✗ Failed after all retries: {e}") + + +def example_connection_validation(): + """Example: Validating connection before operations.""" + print("\n3. Connection Validation") + print("-" * 40) + + base_url = "http://localhost:8082/v1alpha2" + username = "admin" + password = "" + + try: + with SymphonyAPI(base_url, username, password) as client: + # Validate connection first + print(" Checking connection...") + if not client.health_check(): + print(" ✗ Cannot connect to Symphony API") + print(" Check:") + print(f" - URL is correct: {base_url}") + print(" - Symphony is running") + print(" - Network connectivity") + return + + print(" ✓ Connection successful") + + # Validate authentication + print(" Checking authentication...") + try: + client.authenticate() + print(" ✓ Authentication successful") + except SymphonyAPIError as e: + if e.status_code == 401: + print(" ✗ Authentication failed") + print(" Check:") + print(" - Username is correct") + print(" - Password is correct") + print(" - User has necessary permissions") + return + + # Now safe to perform operations + print(" ✓ Ready to perform operations") + + except Exception as e: + print(f"✗ Unexpected error: {e}") + + +def example_timeout_handling(): + """Example: Handling timeouts.""" + print("\n4. Timeout Handling") + print("-" * 40) + + base_url = "http://localhost:8082/v1alpha2" + username = "admin" + password = "" + + # Create client with custom timeout + client = SymphonyAPI( + base_url, + username, + password, + timeout=5.0, # 5 second timeout + ) + + try: + # This might timeout for slow operations + result = client.list_targets() + print("✓ Operation completed within timeout") + + except SymphonyAPIError as e: + if "timeout" in str(e).lower(): + print("✗ Operation timed out") + print(" Consider:") + print(" - Increasing timeout value") + print(" - Checking network connectivity") + print(" - Checking server performance") + else: + print(f"✗ Error: {e}") + + finally: + client.close() + + +def example_input_validation(): + """Example: Input validation best practices.""" + print("\n5. Input Validation") + print("-" * 40) + + def validate_target_name(name: str) -> bool: + """Validate target name format.""" + if not name: + logger.error("Target name cannot be empty") + return False + + if len(name) > 253: + logger.error("Target name too long (max 253 characters)") + return False + + # Symphony typically uses lowercase DNS names + if not name.islower(): + logger.warning("Target name should be lowercase") + + # Check for invalid characters + import re + + if not re.match(r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", name): + logger.error("Target name contains invalid characters") + return False + + return True + + # Test validation + test_names = [ + "valid-target-001", + "Invalid-Name", # Has uppercase + "", # Empty + "target_with_underscore", # Has underscore + "valid-target", + ] + + for name in test_names: + is_valid = validate_target_name(name) + status = "✓" if is_valid else "✗" + print(f" {status} '{name}': {'valid' if is_valid else 'invalid'}") + + +def example_comprehensive_error_handler(): + """Example: Comprehensive error handling wrapper.""" + print("\n6. Comprehensive Error Handler") + print("-" * 40) + + def safe_api_call(client: SymphonyAPI, operation: str, func, *args, **kwargs): + """Safely execute an API call with comprehensive error handling. + + Args: + client: SymphonyAPI client + operation: Description of operation for logging + func: Function to call + *args, **kwargs: Arguments for the function + """ + try: + logger.info(f"Starting operation: {operation}") + result = func(*args, **kwargs) + logger.info(f"✓ {operation} succeeded") + return result + + except SymphonyAPIError as e: + logger.error(f"✗ {operation} failed: {e}") + + # Categorize error + if e.status_code: + if e.status_code == 400: + logger.error(" Bad request - check input parameters") + elif e.status_code == 401: + logger.error(" Authentication failed - check credentials") + elif e.status_code == 403: + logger.error(" Permission denied - check user permissions") + elif e.status_code == 404: + logger.error(" Resource not found") + elif e.status_code == 409: + logger.error(" Conflict - resource may already exist") + elif 500 <= e.status_code < 600: + logger.error(" Server error - may be transient, consider retry") + + # Log details for debugging + if e.response_text: + logger.debug(f" Response: {e.response_text}") + + raise + + except Exception as e: + logger.error(f"✗ {operation} failed with unexpected error: {e}") + raise + + # Example usage + base_url = "http://localhost:8082/v1alpha2" + username = "admin" + password = "" + + with SymphonyAPI(base_url, username, password) as client: + # Use the wrapper + try: + targets = safe_api_call(client, "List targets", client.list_targets) + print(f" Found {len(targets.get('items', []))} targets") + except Exception: + print(" Operation failed - check logs for details") + + +def example_graceful_degradation(): + """Example: Graceful degradation when services are unavailable.""" + print("\n7. Graceful Degradation") + print("-" * 40) + + base_url = "http://localhost:8082/v1alpha2" + username = "admin" + password = "" + + class TargetManager: + """Manager with graceful degradation.""" + + def __init__(self, client: SymphonyAPI): + self.client = client + self.cache: Dict[str, Any] = {} + self.degraded_mode = False + + def get_target(self, name: str) -> Optional[Dict[str, Any]]: + """Get target with fallback to cache.""" + if not self.degraded_mode: + try: + target = self.client.get_target(name) + self.cache[name] = target + return target + except SymphonyAPIError as e: + logger.warning(f"API call failed: {e}") + logger.info("Entering degraded mode, using cache") + self.degraded_mode = True + + # Fallback to cache + if name in self.cache: + logger.info(f"Returning cached data for {name}") + return self.cache[name] + + logger.error(f"No cached data available for {name}") + return None + + try: + with SymphonyAPI(base_url, username, password) as client: + manager = TargetManager(client) + + # Try to get target (will cache if successful) + target = manager.get_target("my-target") + if target: + print(" ✓ Got target (from API or cache)") + else: + print(" ✗ Target not available") + + except Exception as e: + print(f" ✗ Error: {e}") + + +if __name__ == "__main__": + print("=" * 60) + print("Symphony SDK - Error Handling and Best Practices") + print("=" * 60 + "\n") + + # Note: These examples demonstrate error handling patterns + # They won't actually run without valid Symphony credentials + + print("These examples demonstrate error handling patterns.") + print("Update credentials and uncomment function calls to test.\n") + + # Uncomment to run examples: + example_basic_error_handling() + example_retry_logic() + example_connection_validation() + example_timeout_handling() + example_input_validation() + example_comprehensive_error_handler() + example_graceful_degradation() + + print("\n" + "=" * 60) + print("Review the code to see error handling patterns") + print("=" * 60) diff --git a/sdks/symphony-python/examples/README.md b/sdks/symphony-python/examples/README.md new file mode 100644 index 000000000..85260ef73 --- /dev/null +++ b/sdks/symphony-python/examples/README.md @@ -0,0 +1,290 @@ +# Symphony Python SDK Examples + +Example programs to demonstrate what the SDK is capable of. + +## Prerequisites + +```bash +# Install from source +pip install -e . +``` + +## Examples Overview + +### 1. Basic Client Usage ([01_basic_client.py](01_basic_client.py)) + +Learn the fundamentals of using the Symphony API client: +- Initializing the client +- Authentication +- Health checks +- Context manager usage +- Session management + +**Key concepts:** +```python +from symphony_sdk import SymphonyAPI + +with SymphonyAPI(base_url, username, password) as client: + # Authentication happens automatically + if client.health_check(): + print("Connected!") +``` + +### 2. Target Management ([02_target_management.py](02_target_management.py)) + +Working with Symphony targets (devices, edge nodes, etc.): +- Registering new targets +- Listing all targets +- Getting target details +- Updating target status +- Sending heartbeat pings +- Unregistering targets +- Using dataclasses for type safety + +**Key concepts:** +```python +# Register a target +target_spec = { + "displayName": "IoT Device", + "properties": {"location": "datacenter-1"} +} +client.register_target("device-001", target_spec) + +# Or use dataclasses +from symphony_sdk import TargetSpec, TargetState +target = TargetSpec(displayName="IoT Device", ...) +``` + +### 3. Solution and Instance Management ([03_solution_deployment.py](03_solution_deployment.py)) + +Managing solutions and deploying instances: +- Creating solutions with YAML specs +- Creating instances from solutions +- Applying deployments +- Checking deployment status +- Listing solutions and instances +- Resource cleanup +- Using InstanceSpec dataclass + +**Key concepts:** +```python +# Create a solution +solution_yaml = yaml.dump({...}) +client.create_solution("web-app", solution_yaml) + +# Create an instance +instance_spec = { + "solution": "web-app", + "target": {"name": "device-001"} +} +client.create_instance("web-app-prod", instance_spec) +``` + +### 4. COA Provider ([04_coa_provider.py](04_coa_provider.py)) + +Working with Cloud Object API (COA) requests and responses: +- Creating COA requests and responses +- Handling different content types (JSON, text, binary) +- Body encoding/decoding +- Building providers +- Working with DeploymentSpec + +**Key concepts:** +```python +from symphony_sdk import COARequest, COAResponse, State + +# Create a request +request = COARequest(method="GET", route="/components") +request.set_body({"filter": "active"}) + +# Create a response +response = COAResponse.success({"components": [...]}) +response = COAResponse.error("Not found", State.NOT_FOUND) +``` + +### 5. Summary and Status Tracking ([05_summary_tracking.py](05_summary_tracking.py)) + +Tracking deployment progress and status: +- Creating summary results +- Tracking component status +- Handling successful/failed deployments +- Incremental status updates +- Generating status messages +- Serialization + +**Key concepts:** +```python +from symphony_sdk import ( + SummaryResult, SummarySpec, SummaryState, + create_success_component_result, + create_failed_component_result +) + +# Track deployment +summary = SummarySpec(target_count=2, planned_deployment=4) +summary.update_target_result("target-1", target_result) + +result = SummaryResult(summary=summary, state=SummaryState.DONE) +if result.is_deployment_finished(): + print("Deployment complete!") +``` + +### 6. Error Handling and Best Practices ([06_error_handling.py](06_error_handling.py)) + +Production-ready error handling patterns: +- Catching and handling SymphonyAPIError +- Implementing retry logic with exponential backoff +- Connection and authentication validation +- Timeout handling +- Input validation +- Comprehensive error handling wrappers +- Graceful degradation strategies + +**Key concepts:** +```python +from symphony_sdk import SymphonyAPIError + +# Basic error handling +try: + client.register_target("device", spec) +except SymphonyAPIError as e: + if e.status_code == 404: + print("Not found") + elif e.status_code == 500: + print("Server error - retry") + +# Retry with exponential backoff +def retry_with_backoff(func, max_retries=3): + for attempt in range(max_retries): + try: + return func() + except SymphonyAPIError as e: + if attempt < max_retries - 1: + time.sleep(2 ** attempt) + raise +``` + +## Running the Examples + +### Update Credentials + +Most examples require you to update the credentials before running: + +```python +# Update these values +base_url = "https://your-symphony-instance.com" +username = "your-username" +password = "your-password" +``` + +### Run an Example + +```bash +# Make the script executable +chmod +x examples/01_basic_client.py + +# Run it +python examples/01_basic_client.py +``` + +### COA Provider Example + +The COA provider example can be run directly without credentials: + +```bash +python examples/04_coa_provider.py +``` + +### Summary Tracking Example + +The summary tracking example demonstrates data structures and can also be run directly: + +```bash +python examples/05_summary_tracking.py +``` + +## Common Patterns + +### Error Handling + +```python +from symphony_sdk import SymphonyAPIError + +try: + client.register_target(name, spec) +except SymphonyAPIError as e: + print(f"Error: {e}") + print(f"Status code: {e.status_code}") + print(f"Response: {e.response_text}") +``` + +### Using Context Managers + +```python +# Recommended: Ensures proper cleanup +with SymphonyAPI(base_url, username, password) as client: + # Your code here + pass +# Session automatically closed +``` + +### Working with Dataclasses + +```python +from symphony_sdk import TargetSpec, ComponentSpec, InstanceSpec + +# Type-safe approach +component = ComponentSpec( + name="nginx", + type="container", + properties={"image": "nginx:latest"} +) + +target = TargetSpec( + displayName="My Device", + components=[component] +) +``` + +### Checking Deployment Status + +```python +import time + +# Poll for completion +for attempt in range(10): + status = client.get_instance_status(instance_name) + if status.get('status') == 'Succeeded': + break + time.sleep(2) +``` + +## Data Models + +The SDK provides comprehensive data models matching Symphony's COA specification: + +- **Target Models**: `TargetSpec`, `TargetState`, `TargetStatus` +- **Solution Models**: `SolutionSpec`, `SolutionState`, `ComponentSpec` +- **Instance Models**: `InstanceSpec`, `DeploymentSpec` +- **COA Models**: `COARequest`, `COAResponse`, `State` +- **Summary Models**: `SummaryResult`, `SummarySpec`, `ComponentResultSpec` + +See [models.py](../src/symphony_sdk/models.py), [types.py](../src/symphony_sdk/types.py), and [summary.py](../src/symphony_sdk/summary.py) for full details. + +## Next Steps + +1. Review the [main README](../README.md) for installation and setup +2. Check the [API documentation](../docs/API.md) for detailed API reference +3. Explore the [test files](../tests/) for more usage examples +4. Read the Symphony documentation for architecture concepts + +## Need Help? + +- Check the [Symphony documentation](https://github.com/eclipse-symphony/symphony) +- Review the source code in [src/symphony_sdk/](../src/symphony_sdk/) +- Look at the unit tests in [tests/](../tests/) +- Open an issue on the Symphony GitHub repository + +## Contributing + +Found a bug or want to add an example? Contributions are welcome! Please follow the Symphony contribution guidelines. diff --git a/sdks/symphony-python/pyproject.toml b/sdks/symphony-python/pyproject.toml new file mode 100644 index 000000000..c4bea049d --- /dev/null +++ b/sdks/symphony-python/pyproject.toml @@ -0,0 +1,115 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "symphony-sdk" +version = "0.1.0" +description = "Pure Python SDK for Eclipse Symphony" +readme = "README.md" +requires-python = ">=3.9" +authors = [ + { name = "composiv.ai", email = "info@composiv.ai" } +] +dependencies = [ + "requests>=2.25", + "PyYAML>=6.0", +] + +[project.optional-dependencies] +mqtt = [ + "paho-mqtt>=2.0", +] +dev = [ + "pytest>=7.0", + "pytest-cov>=4.0", + "mypy>=1.0", + "ruff>=0.1.0", + "types-PyYAML>=6.0", + "types-requests>=2.25", +] + +[project.urls] +Homepage = "https://github.com/eclipse-symphony/symphony" +Issues = "https://github.com/eclipse-symphony/symphony/issues" + +[tool.hatch.build.targets.wheel] +packages = ["src/symphony_sdk"] + +[tool.hatch.build.targets.sdist] +include = [ + "src/symphony_sdk", + "README.md", + "LICENSE" +] + +[tool.uv.sources] +symphony-sdk = { workspace = true } + +[dependency-groups] +dev = [ + "symphony-sdk", + "twine>=6.2.0", +] + +[tool.ruff] +# Enable pycodestyle (E), Pyflakes (F), isort (I), pydocstyle (D), and more +line-length = 100 +target-version = "py39" + +[tool.ruff.lint] +# Allow unused variables when underscore-prefixed +select = ["E", "F", "W", "I", "D", "UP", "B", "C4", "SIM"] +ignore = ["D203", "D213"] # Ignore conflicting docstring rules +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] # Allow unused imports in __init__.py +"tests/*" = ["D"] # Ignore docstring requirements in tests + +[tool.ruff.lint.pydocstyle] +convention = "google" # Use Google-style docstrings + +[tool.mypy] +python_version = "3.9" +warn_return_any = true +warn_unused_configs = true +warn_redundant_casts = true +warn_unused_ignores = true +disallow_untyped_defs = false # Set to true once all code is typed +disallow_any_unimported = false +no_implicit_optional = true +strict_optional = true +check_untyped_defs = true +ignore_missing_imports = false + +[[tool.mypy.overrides]] +module = "tests.*" +disallow_untyped_defs = false + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = "test_*.py" +python_classes = "Test*" +python_functions = "test_*" +addopts = [ + "--verbose", + "--cov=src/symphony_sdk", + "--cov-report=term-missing", + "--cov-report=html", +] + +[tool.coverage.run] +source = ["src/symphony_sdk"] +omit = ["*/tests/*", "*/test_*.py", "*/__init__.py"] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", + "@abstractmethod", +] diff --git a/sdks/symphony-python/src/symphony_sdk/__init__.py b/sdks/symphony-python/src/symphony_sdk/__init__.py new file mode 100644 index 000000000..680061ddd --- /dev/null +++ b/sdks/symphony-python/src/symphony_sdk/__init__.py @@ -0,0 +1,17 @@ +"""Public Symphony SDK interface.""" + +from . import models as _models +from . import summary as _summary +from . import types as _types +from .api_client import SymphonyAPI, SymphonyAPIError +from .models import * # noqa: F401,F403 +from .summary import * # noqa: F401,F403 +from .types import * # noqa: F401,F403 + +__all__ = [ + "SymphonyAPI", + "SymphonyAPIError", + *_models.__all__, + *_summary.__all__, + *_types.__all__, +] diff --git a/sdks/symphony-python/src/symphony_sdk/api_client.py b/sdks/symphony-python/src/symphony_sdk/api_client.py new file mode 100644 index 000000000..e75b6d8e2 --- /dev/null +++ b/sdks/symphony-python/src/symphony_sdk/api_client.py @@ -0,0 +1,639 @@ +"""Symphony REST API Client. + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +SPDX-License-Identifier: MIT + +This module provides a client for interacting with the Symphony REST API, +including authentication, target registration/unregistration, and other +Symphony operations based on the OpenAPI specification. +""" + +import json +import logging +from datetime import datetime, timezone, timedelta +from typing import Any, Dict, List, Optional, Union + +JSONType = Union[Dict[str, Any], List[Dict[str, Any]]] + +import requests + + +class SymphonyAPIError(Exception): + """Custom exception for Symphony API errors.""" + + def __init__( + self, message: str, status_code: Optional[int] = None, response_text: Optional[str] = None + ): + super().__init__(message) + self.status_code = status_code + self.response_text = response_text + + +class SymphonyAPI: + """Symphony REST API Client. + + Provides methods for interacting with the Symphony API including: + - Authentication + - Target management (register/unregister) + - Solution management + - Instance management + - Other Symphony operations + """ + + def __init__( + self, + base_url: str, + username: str, + password: str, + timeout: float = 30.0, + logger: Optional[logging.Logger] = None, + ): + """Initialize the Symphony API client. + + Args: + base_url: Base URL of the Symphony API (e.g., 'https://symphony.example.com') + username: Symphony username for authentication + password: Symphony password for authentication + timeout: Request timeout in seconds + logger: Optional logger instance + """ + self.base_url = base_url.rstrip("/") + self.username = username + self.password = password + self.timeout = timeout + self.logger = logger or logging.getLogger(__name__) + + # Authentication state + self._access_token: Optional[str] = None + self._token_expiry: Optional[datetime] = None + + # Session for connection reuse + self._session = requests.Session() + self._session.headers.update( + {"Content-Type": "application/json", "User-Agent": "SymphonySDK/0.1.0"} + ) + + def __enter__(self): + """Context manager entry.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit - close session.""" + self.close() + + def close(self): + """Close the HTTP session.""" + if self._session: + self._session.close() + + def _make_request(self, method: str, endpoint: str, **kwargs) -> requests.Response: + """Make an HTTP request to the Symphony API. + + Args: + method: HTTP method (GET, POST, PUT, DELETE, etc.) + endpoint: API endpoint (without base URL) + **kwargs: Additional arguments for requests + + Returns: + requests.Response object + + Raises: + SymphonyAPIError: If the request fails + """ + url = f"{self.base_url}/{endpoint.lstrip('/')}" + + # Set default timeout + kwargs.setdefault("timeout", self.timeout) + + try: + self.logger.debug(f"Making {method} request to {url}") + response = self._session.request(method, url, **kwargs) + + self.logger.debug(f"Response: {response.status_code} {response.reason}") + + return response + + except requests.exceptions.RequestException as e: + self.logger.error(f"Request failed: {e}") + raise SymphonyAPIError(f"Request failed: {str(e)}") + + def _handle_response( + self, response: requests.Response, expected_codes: List[int] = None + ) -> Optional[JSONType]: + """Handle API response and extract JSON data. + + Args: + response: requests.Response object + expected_codes: List of expected HTTP status codes (default: [200]) + + Returns: + Parsed JSON response data + + Raises: + SymphonyAPIError: If response indicates an error + """ + if expected_codes is None: + expected_codes = [200] + + if response.status_code not in expected_codes: + error_msg = f"API request failed with status {response.status_code}: {response.reason}" + self.logger.error(error_msg) + self.logger.error(f"Response body: {response.text}") + raise SymphonyAPIError(error_msg, response.status_code, response.text) + + # Handle empty responses + if not response.content.strip(): + return {} + + try: + return response.json() + except json.JSONDecodeError as e: + self.logger.error(f"Failed to parse JSON response: {e}") + self.logger.error(f"Response body: {response.text}") + raise SymphonyAPIError(f"Invalid JSON response: {str(e)}") + + def authenticate(self, force_refresh: bool = False) -> str: + """Authenticate with Symphony API and return access token. + + Args: + force_refresh: Force token refresh even if current token is valid + + Returns: + Access token string + + Raises: + SymphonyAPIError: If authentication fails + """ + # Check if we have a valid token + if not force_refresh and self._access_token and self._token_expiry: + # Use timezone-aware UTC datetimes + if datetime.now(timezone.utc) < self._token_expiry: + return self._access_token + + self.logger.info(f"Authenticating with Symphony API as user '{self.username}'") + + auth_payload = {"username": self.username, "password": self.password} + + response = self._make_request("POST", "/users/auth", json=auth_payload) + data = self._handle_response(response) + + access_token = data.get("accessToken") + if not access_token: + raise SymphonyAPIError("No access token in authentication response") + + self._access_token = access_token + # Assume token is valid for ~1 hour + # Use timezone-aware UTC datetimes and truncate seconds/micros + self._token_expiry = ( + datetime.now(timezone.utc) + .replace(second=0, microsecond=0) + + timedelta(minutes=50) + ) + # Update session headers with token + self._session.headers.update({"Authorization": f"Bearer {access_token}"}) + + self.logger.info("Successfully authenticated with Symphony API") + return access_token + + def _ensure_authenticated(self): + """Ensure we have a valid authentication token.""" + if not self._access_token: + self.authenticate() + # Token refresh is handled automatically in authenticate() + + # Target Management Methods + def register_target(self, target_name: str, target_spec: Dict[str, Any]) -> Dict[str, Any]: + """Register a target with Symphony. + + Args: + target_name: Name of the target to register + target_spec: Target specification dictionary (should contain fields like + displayName, scope, properties, components, metadata, etc.) + + Returns: + API response data + + Raises: + SymphonyAPIError: If registration fails + """ + self._ensure_authenticated() + + self.logger.info(f"Registering target '{target_name}' with Symphony") + + # Wrap the spec in a TargetState structure as expected by the API + target_state = {"metadata": {"name": target_name}, "spec": target_spec} + + response = self._make_request("POST", f"/targets/registry/{target_name}", json=target_state) + + data = self._handle_response(response, [200, 201]) + self.logger.info(f"Successfully registered target '{target_name}'") + + return data + + def unregister_target(self, target_name: str, direct: bool = False) -> Dict[str, Any]: + """Unregister a target from Symphony. + + Args: + target_name: Name of the target to unregister + direct: Whether to use direct delete + + Returns: + API response data + + Raises: + SymphonyAPIError: If unregistration fails + """ + self._ensure_authenticated() + + self.logger.info(f"Unregistering target '{target_name}' from Symphony") + + params = {"direct": "true"} if direct else {} + + response = self._make_request("DELETE", f"/targets/registry/{target_name}", params=params) + + data = self._handle_response(response, [200, 204]) + self.logger.info(f"Successfully unregistered target '{target_name}'") + + return data + + def get_target(self, target_name: str) -> Dict[str, Any]: + """Get target specification. + + Args: + target_name: Name of the target + + Returns: + Target specification data + + Raises: + SymphonyAPIError: If request fails + """ + self._ensure_authenticated() + + response = self._make_request( + "GET", + f"/targets/registry/{target_name}", + ) + + return self._handle_response(response) + + def list_targets(self) -> List[Dict[str, Any]]: + """List all registered targets. + + Returns: + List of targets + + Raises: + SymphonyAPIError: If request fails + """ + self._ensure_authenticated() + + response = self._make_request("GET", "/targets/registry") + return self._handle_response(response) + + def ping_target(self, target_name: str) -> Dict[str, Any]: + """Send heartbeat ping to target. + + Args: + target_name: Name of the target to ping + + Returns: + Ping response data + + Raises: + SymphonyAPIError: If request fails + """ + self._ensure_authenticated() + + response = self._make_request("POST", f"/targets/ping/{target_name}") + return self._handle_response(response) + + def update_target_status(self, target_name: str, status_data: Dict[str, Any]) -> Dict[str, Any]: + """Update target status. + + Args: + target_name: Name of the target + status_data: Status information dictionary + + Returns: + API response data + + Raises: + SymphonyAPIError: If request fails + """ + self._ensure_authenticated() + + response = self._make_request("PUT", f"/targets/status/{target_name}", json=status_data) + + return self._handle_response(response) + + # Solution Management Methods + + def create_solution( + self, + solution_name: str, + solution_spec: str, + namespace: str = "default", + embed_type: Optional[str] = None, + embed_component: Optional[str] = None, + embed_property: Optional[str] = None, + ) -> Dict[str, Any]: + """Create a solution with embedded specification. + + Args: + solution_name: Name of the solution + solution_spec: Solution specification as YAML/JSON text (will be parsed) + namespace: Namespace/scope for the solution (default: "default") + embed_type: Optional embed type + embed_component: Optional embed component + embed_property: Optional embed property + + Returns: + API response data + + Raises: + SymphonyAPIError: If request fails + """ + self._ensure_authenticated() + + params = {"namespace": namespace} + if embed_type: + params["embed-type"] = embed_type + if embed_component: + params["embed-component"] = embed_component + if embed_property: + params["embed-property"] = embed_property + + # Parse the YAML/JSON spec and wrap it in SolutionState structure + try: + import yaml + + spec_dict = yaml.safe_load(solution_spec) + except Exception as e: + raise SymphonyAPIError(f"Failed to parse solution specification: {str(e)}") + + # Wrap in SolutionState structure expected by the API + solution_state = { + "metadata": {"name": solution_name, "namespace": namespace}, + "spec": spec_dict, + } + + response = self._make_request( + "POST", f"/solutions/{solution_name}", json=solution_state, params=params + ) + + return self._handle_response(response, [200, 201]) + + def get_solution(self, solution_name: str, namespace: str = "default") -> Dict[str, Any]: + """Get solution specification. + + Args: + solution_name: Name of the solution + namespace: Namespace/scope of the solution (default: "default") + + Returns: + Solution specification data (returns the full SolutionState with metadata and spec) + + Raises: + SymphonyAPIError: If request fails + """ + self._ensure_authenticated() + + params = {"namespace": namespace} + + response = self._make_request("GET", f"/solutions/{solution_name}", params=params) + + # Return the full solution state (metadata + spec) + return self._handle_response(response) + + def delete_solution(self, solution_name: str) -> Dict[str, Any]: + """Delete a solution. + + Args: + solution_name: Name of the solution to delete + + Returns: + API response data + + Raises: + SymphonyAPIError: If request fails + """ + self._ensure_authenticated() + + response = self._make_request("DELETE", f"/solutions/{solution_name}") + return self._handle_response(response) + + def list_solutions(self) -> List[Dict[str, Any]]: + """List all solutions. + + Returns: + List of solutions + + Raises: + SymphonyAPIError: If request fails + """ + self._ensure_authenticated() + + response = self._make_request("GET", "/solutions") + return self._handle_response(response) + + # Instance Management Methods + + def create_instance( + self, instance_name: str, instance_spec: Dict[str, Any], namespace: str = "default" + ) -> Dict[str, Any]: + """Create an instance. + + Args: + instance_name: Name of the instance + instance_spec: Instance specification dictionary (should contain solution, target, etc.) + namespace: Namespace/scope for the instance (default: "default") + + Returns: + API response data + + Raises: + SymphonyAPIError: If request fails + """ + self._ensure_authenticated() + + # Wrap the spec in InstanceState structure as expected by the API + instance_state = { + "metadata": {"name": instance_name, "namespace": namespace}, + "spec": instance_spec, + } + + params = {"namespace": namespace} + + response = self._make_request( + "POST", f"/instances/{instance_name}", json=instance_state, params=params + ) + + return self._handle_response(response, [200, 201]) + + def get_instance(self, instance_name: str, namespace: str = "default") -> Dict[str, Any]: + """Get instance specification. + + Args: + instance_name: Name of the instance + namespace: Namespace/scope of the instance (default: "default") + + Returns: + Instance specification data (returns the full InstanceState with metadata and spec) + + Raises: + SymphonyAPIError: If request fails + """ + self._ensure_authenticated() + + params = {"namespace": namespace} + + response = self._make_request("GET", f"/instances/{instance_name}", params=params) + + # Return the full instance state (metadata + spec) + return self._handle_response(response) + + def delete_instance(self, instance_name: str) -> Dict[str, Any]: + """Delete an instance. + + Args: + instance_name: Name of the instance to delete + + Returns: + API response data + + Raises: + SymphonyAPIError: If request fails + """ + self._ensure_authenticated() + + response = self._make_request("DELETE", f"/instances/{instance_name}") + return self._handle_response(response) + + def list_instances(self) -> List[Dict[str, Any]]: + """List all instances. + + Returns: + List of instances + + Raises: + SymphonyAPIError: If request fails + """ + self._ensure_authenticated() + + response = self._make_request("GET", "/instances") + return self._handle_response(response) + + # Solution Operations (for deployments) + + def apply_deployment(self, deployment_spec: Dict[str, Any]) -> Dict[str, Any]: + """Apply a deployment (POST to /solution/instances). + + Args: + deployment_spec: Deployment specification dictionary + + Returns: + API response data + + Raises: + SymphonyAPIError: If request fails + """ + self._ensure_authenticated() + + response = self._make_request("POST", "/solution/instances", json=deployment_spec) + return self._handle_response(response) + + def get_deployment_components(self) -> Dict[str, Any]: + """Get deployment components (GET /solution/instances). + + Returns: + Components data + + Raises: + SymphonyAPIError: If request fails + """ + self._ensure_authenticated() + + response = self._make_request("GET", "/solution/instances") + return self._handle_response(response) + + def delete_deployment_components(self) -> Dict[str, Any]: + """Delete deployment components (DELETE /solution/instances). + + Returns: + API response data + + Raises: + SymphonyAPIError: If request fails + """ + self._ensure_authenticated() + + response = self._make_request("DELETE", "/solution/instances") + return self._handle_response(response) + + def reconcile_solution( + self, deployment_spec: Dict[str, Any], delete: bool = False + ) -> Dict[str, Any]: + """Direct reconcile/delete deployment (POST to /solution/reconcile). + + Args: + deployment_spec: Deployment specification dictionary + delete: Whether this is a delete operation + + Returns: + API response data + + Raises: + SymphonyAPIError: If request fails + """ + self._ensure_authenticated() + + params = {"delete": "true"} if delete else {} + + response = self._make_request( + "POST", "/solution/reconcile", json=deployment_spec, params=params + ) + + return self._handle_response(response) + + def get_instance_status(self, instance_name: str) -> Dict[str, Any]: + """Get instance status (GET /solution/queue). + + Args: + instance_name: Name of the instance + + Returns: + Instance status data + + Raises: + SymphonyAPIError: If request fails + """ + self._ensure_authenticated() + + params = {"instance": instance_name} + + response = self._make_request("GET", "/solution/queue", params=params) + return self._handle_response(response) + + # Utility Methods + + def health_check(self) -> bool: + """Perform a basic health check of the Symphony API. + + Returns: + True if API is accessible, False otherwise + """ + try: + # Try a simple endpoint that doesn't require auth + response = self._make_request("GET", "/greetings") + return response.status_code == 200 + except Exception as e: + self.logger.error(f"Health check failed: {e}") + return False + + +__all__ = [ + "SymphonyAPI", + "SymphonyAPIError", +] diff --git a/sdks/symphony-python/src/symphony_sdk/models.py b/sdks/symphony-python/src/symphony_sdk/models.py new file mode 100644 index 000000000..ae3007a2c --- /dev/null +++ b/sdks/symphony-python/src/symphony_sdk/models.py @@ -0,0 +1,655 @@ +"""Symphony SDK data structures and utilities. + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +SPDX-License-Identifier: MIT + +This module provides Symphony-compatible data structures and helpers +following the official Eclipse Symphony COA patterns. +""" + +# Standard library imports +import base64 +import json +import logging +from dataclasses import dataclass, field +from enum import Enum +from typing import Any, Dict, List, Optional, get_args, get_origin + +from symphony_sdk.types import State + +logger = logging.getLogger(__name__) + + +@dataclass +class ObjectMeta: + """Object metadata following Kubernetes-style metadata.""" + + namespace: str = "" + name: str = "" + labels: Optional[Dict[str, str]] = None + annotations: Optional[Dict[str, str]] = None + + +@dataclass +class TargetSelector: + """Target selector for component binding.""" + + name: str = "" + selector: Optional[Dict[str, str]] = None + + +@dataclass +class BindingSpec: + """Binding specification for component deployment.""" + + role: str = "" + provider: str = "" + config: Optional[Dict[str, str]] = None + + +@dataclass +class TopologySpec: + """Topology specification for deployment.""" + + device: str = "" + selector: Optional[Dict[str, str]] = None + bindings: Optional[List[BindingSpec]] = None + + +@dataclass +class PipelineSpec: + """Pipeline specification for data processing.""" + + name: str = "" + skill: str = "" + parameters: Optional[Dict[str, str]] = None + + +@dataclass +class VersionSpec: + """Version specification for solutions.""" + + solution: str = "" + percentage: int = 100 + + +@dataclass +class InstanceSpec: + """Instance specification for Symphony deployments.""" + + name: str = "" + parameters: Optional[Dict[str, str]] = None + solution: str = "" + target: Optional[TargetSelector] = None + topologies: Optional[List[TopologySpec]] = None + pipelines: Optional[List[PipelineSpec]] = None + scope: str = "" + display_name: str = "" + metadata: Optional[Dict[str, str]] = None + versions: Optional[List[VersionSpec]] = None + arguments: Optional[Dict[str, Dict[str, str]]] = None + opt_out_reconciliation: bool = False + + +@dataclass +class FilterSpec: + """Filter specification for routing.""" + + direction: str = "" + parameters: Optional[Dict[str, str]] = None + type: str = "" + + +@dataclass +class RouteSpec: + route: str = "" + properties: Dict[str, str] = None + filters: List[FilterSpec] = None + type: str = "" + + +@dataclass +class ComponentSpec: + name: str = "" + type: str = "" + routes: List[RouteSpec] = None + constraints: str = "" + properties: Dict[str, str] = None + dependencies: List[str] = None + skills: List[str] = None + metadata: Dict[str, str] = None + parameters: Dict[str, str] = None + + +@dataclass +class SolutionSpec: + components: List[ComponentSpec] = None + scope: str = "" + displayName: str = "" + metadata: Dict[str, str] = None + + +@dataclass +class SolutionState: + metadata: ObjectMeta = None + spec: SolutionSpec = None + + +@dataclass +class TargetSpec: + properties: Dict[str, str] = None + components: List[ComponentSpec] = None + constraints: str = "" + topologies: List[TopologySpec] = None + scope: str = "" + displayName: str = "" + metadata: Dict[str, str] = None + forceRedeploy: bool = False + + +@dataclass +class ComponentError: + code: str = "" + message: str = "" + target: str = "" + + +@dataclass +class TargetError: + code: str = "" + message: str = "" + target: str = "" + details: Dict[str, ComponentError] = None + + +@dataclass +class ErrorType: + code: str = "" + message: str = "" + target: str = "" + details: Dict[str, TargetError] = None + + +@dataclass +class ProvisioningStatus: + operationId: str = "" + status: str = "" + failureCause: str = "" + logErrors: bool = False + error: ErrorType = None + output: Dict[str, str] = None + + +@dataclass +class TargetStatus: + properties: Dict[str, str] = None + provisioningStatus: ProvisioningStatus = None + lastModififed: str = "" + + +@dataclass +class TargetState: + metadata: ObjectMeta = None + spec: TargetSpec = None + status: TargetStatus = None + + +@dataclass +class DeviceSpec: + properties: Dict[str, str] = None + bindings: List[BindingSpec] = None + displayName: str = "" + + +@dataclass +class DeploymentSpec: + solutionName: str = "" + solution: SolutionState = None + instance: InstanceSpec = None + targets: Dict[str, TargetState] = None + devices: List[DeviceSpec] = None + assignments: Dict[str, str] = None + componentStartIndex: int = -1 + componentEndIndex: int = -1 + activeTarget: str = "" + + def get_components_slice(self) -> List[ComponentSpec]: + if self.solution != None: + if ( + self.componentStartIndex >= 0 + and self.componentEndIndex >= 0 + and self.componentEndIndex > self.componentStartIndex + ): + return self.solution.spec.components[ + self.componentStartIndex : self.componentEndIndex + ] + return self.solution.spec.components + return [] + + +@dataclass +class ComparisonPack: + desired: List[ComponentSpec] + current: List[ComponentSpec] + + +@dataclass +class COABodyMixin: + """Common functionality for COA request and response body handling. + + Provides content-type aware body encoding/decoding with support for: + - "application/json": JSON objects + - "text/plain": Plain text strings + - "application/octet-stream": Binary data + """ + + content_type: str = "application/json" + body: str = "" # Base64 encoded data (type determined by content_type) + + def set_body(self, data: Any, content_type: Optional[str] = None) -> None: + """Set body data with content type detection or explicit content type. + + Args: + data: Data to set (JSON object, string, or bytes) + content_type: Optional explicit content type override + """ + # Set content type if provided + if content_type: + self.content_type = content_type + + if self.content_type == "application/json": + # Handle JSON data + if isinstance(data, (str, bytes)): + # If it's a string/bytes, try to parse as JSON first + if isinstance(data, bytes): + data = data.decode("utf-8") + json.loads(data) # Validate JSON + self.body = base64.b64encode(data.encode("utf-8")).decode("utf-8") + else: + # Serialize object to JSON + json_str = json.dumps(data, ensure_ascii=False) + self.body = base64.b64encode(json_str.encode("utf-8")).decode("utf-8") + + elif self.content_type == "text/plain": + # Handle plain text - no encoding necessary + if isinstance(data, bytes): + self.body = data.decode("utf-8") + else: + self.body = str(data) + + elif self.content_type == "application/octet-stream": + # Handle binary data + if isinstance(data, str): + # If string, assume it's already base64 encoded + self.body = data + elif isinstance(data, bytes): + # Encode bytes to base64 + self.body = base64.b64encode(data).decode("utf-8") + else: + raise ValueError( + f"Binary content type requires bytes or base64 string, got {type(data)}" + ) + + else: + # Default fallback - treat as text + if isinstance(data, bytes): + text_str = data.decode("utf-8") + else: + text_str = str(data) + self.body = base64.b64encode(text_str.encode("utf-8")).decode("utf-8") + + def get_body(self) -> Any: + """Get body data decoded according to content type. + + Returns: + Decoded body data (JSON object, string, or bytes depending on content_type) + """ + if not self.body: + return None + + if self.content_type == "application/json": + # Return parsed JSON object + try: + json_str = base64.b64decode(self.body).decode("utf-8") + return json.loads(json_str) + except (ValueError, json.JSONDecodeError) as e: + raise ValueError(f"Invalid JSON in body: {e}") + + elif self.content_type == "text/plain": + # Return text string directly - no decoding necessary + return self.body + + elif self.content_type == "application/octet-stream": + # Return raw bytes + return base64.b64decode(self.body) + + else: + # Default fallback - return as string + return base64.b64decode(self.body).decode("utf-8") + + +@dataclass +class COARequest(COABodyMixin): + """COA Request structure based on Symphony COA API. + + This corresponds to the Go struct COARequest from Symphony codebase. + The body field contains base64 encoded data, with the content type determined by content_type: + - "application/json": Base64 encoded UTF-8 string of JSON object + - "text/plain": Base64 encoded UTF-8 string of plain text + - "application/octet-stream": Base64 encoded binary data + """ + + method: str = "GET" + route: str = "" + metadata: Optional[Dict[str, str]] = field(default_factory=dict) + parameters: Optional[Dict[str, str]] = field(default_factory=dict) + + def to_json_dict(self) -> Dict[str, Any]: + """Convert to JSON-serializable dictionary.""" + result = { + "method": self.method, + "route": self.route, + "content-type": self.content_type, + "body": self.body, + } + if self.metadata: + result["metadata"] = self.metadata + if self.parameters: + result["parameters"] = self.parameters + return result + + +@dataclass +class COAResponse(COABodyMixin): + """COA Response structure based on Symphony COA API. + + This corresponds to the Go struct COAResponse from Symphony codebase. + The body field contains base64 encoded data, with the content type determined by content_type: + - "application/json": Base64 encoded UTF-8 string of JSON object + - "text/plain": Base64 encoded UTF-8 string of plain text + - "application/octet-stream": Base64 encoded binary data + """ + + state: State = State.OK + metadata: Optional[Dict[str, str]] = field(default_factory=dict) + redirect_uri: Optional[str] = None + + def to_json_dict(self) -> Dict[str, Any]: + """Convert to JSON-serializable dictionary.""" + result = {"content-type": self.content_type, "body": self.body, "state": self.state.value} + if self.metadata: + result["metadata"] = self.metadata + if self.redirect_uri: + result["redirectUri"] = self.redirect_uri + return result + + @classmethod + def success(cls, data: Any = None, content_type: str = "application/json") -> "COAResponse": + """Create a success response.""" + response = cls(content_type=content_type, state=State.OK) + if data is not None: + response.set_body(data, content_type) + return response + + @classmethod + def error( + cls, + message: str, + state: State = State.INTERNAL_ERROR, + content_type: str = "application/json", + ) -> "COAResponse": + """Create an error response.""" + response = cls(content_type=content_type, state=state) + if content_type == "application/json": + response.set_body({"error": message}, content_type) + elif content_type == "text/plain": + response.set_body(f"Error: {message}", content_type) + else: + response.set_body({"error": message}, "application/json") + return response + + @classmethod + def not_found(cls, message: str = "Resource not found") -> "COAResponse": + """Create a not found response.""" + return cls.error(message, State.NOT_FOUND) + + @classmethod + def bad_request(cls, message: str = "Bad request") -> "COAResponse": + """Create a bad request response.""" + return cls.error(message, State.BAD_REQUEST) + + +# Utility functions for COA data conversion +def to_dict(obj: Any) -> Dict[str, Any]: + """Convert dataclass object to dictionary.""" + if obj is None: + return {} + + if hasattr(obj, "__dict__"): + result = {} + for key, value in obj.__dict__.items(): + if value is not None: + if isinstance(value, list): + result[key] = [to_dict(item) for item in value] + elif isinstance(value, dict): + result[key] = {k: to_dict(v) for k, v in value.items()} + elif hasattr(value, "__dict__"): + result[key] = to_dict(value) + elif isinstance(value, Enum): + result[key] = value.value + else: + result[key] = value + return result + + return obj + + +def from_dict(data: Dict[str, Any], cls: type) -> Any: + """Convert dictionary to dataclass object.""" + if not data: + return cls() + + try: + # Handle enums + if isinstance(cls, type) and issubclass(cls, Enum): + return cls(data) + + # Handle basic types + if cls in (str, int, float, bool): + return cls(data) + + # Handle dataclass + if hasattr(cls, "__dataclass_fields__"): + kwargs = {} + for field_name, field_info in cls.__dataclass_fields__.items(): + if field_name in data: + field_type = field_info.type + field_value = data[field_name] + + # Handle Optional types + if get_origin(field_type) is Optional and type(None) in get_args(field_type): + if field_value is None: + kwargs[field_name] = None + else: + inner_type = field_type.__args__[0] + kwargs[field_name] = from_dict(field_value, inner_type) + + # Handle List types + elif hasattr(field_type, "__origin__") and field_type.__origin__ is list: + if field_value and isinstance(field_value, list): + inner_type = field_type.__args__[0] + kwargs[field_name] = [ + from_dict(item, inner_type) for item in field_value + ] + else: + kwargs[field_name] = field_value or [] + + # Handle Dict types + elif hasattr(field_type, "__origin__") and field_type.__origin__ is dict: + kwargs[field_name] = field_value or {} + + # Handle nested dataclasses + elif hasattr(field_type, "__dataclass_fields__"): + kwargs[field_name] = from_dict(field_value, field_type) + + # Handle enums + elif isinstance(field_type, type) and issubclass(field_type, Enum): + kwargs[field_name] = field_type(field_value) + + else: + kwargs[field_name] = field_value + + return cls(**kwargs) + + except Exception: + # Fallback: return default instance + return cls() + + +def serialize_components(components: List[ComponentSpec]) -> str: + """Serialize components to JSON string.""" + return json.dumps([to_dict(comp) for comp in components], indent=2) + + +def deserialize_components(json_str: str) -> List[ComponentSpec]: + """Deserialize JSON string to components list.""" + try: + data = json.loads(json_str) + return [from_dict(item, ComponentSpec) for item in data] + except Exception: + return [] + + +def deserialize_solution(json_str: str) -> List[SolutionState]: + """Deserialize JSON string to components list.""" + try: + data = json.loads(json_str) + return [from_dict(item, SolutionState) for item in data] + except Exception: + return [] + + +# Desrialize a DeploymentSpec object from Json String +def deserialize_deployment(json_str: str) -> List[DeploymentSpec]: + """Deserialize JSON string to DeploymentSpec list.""" + try: + data = json.loads(json_str) + return [from_dict(data, DeploymentSpec)] + except Exception as e: + logger.error("Error deserializing deployment: %s", e) + return [] + + +def serialize_coa_request(coa_request: COARequest) -> str: + """Serialize COARequest to JSON string.""" + return json.dumps(coa_request.to_json_dict(), indent=2) + + +def deserialize_coa_request(json_str: str) -> COARequest: + """Deserialize JSON string to COARequest.""" + try: + data = json.loads(json_str) + request = COARequest() + + # Map JSON fields to dataclass fields + if "method" in data: + request.method = data["method"] + if "route" in data: + request.route = data["route"] + if "content-type" in data: + request.content_type = data["content-type"] + if "body" in data: + body_data = data["body"] + if isinstance(body_data, str): + # Assume it's already base64 encoded JSON string + request.body = body_data + else: + # Convert object to JSON and base64 encode + request.set_body(body_data, "application/json") + if "metadata" in data: + request.metadata = data["metadata"] + if "parameters" in data: + request.parameters = data["parameters"] + + return request + except Exception as e: + logger.error("Error deserializing COA request: %s", e) + return COARequest() + + +def serialize_coa_response(coa_response: COAResponse) -> str: + """Serialize COAResponse to JSON string.""" + return json.dumps(coa_response.to_json_dict(), indent=2) + + +def deserialize_coa_response(json_str: str) -> COAResponse: + """Deserialize JSON string to COAResponse.""" + try: + data = json.loads(json_str) + response = COAResponse() + + # Map JSON fields to dataclass fields + if "content-type" in data: + response.content_type = data["content-type"] + if "body" in data: + body_data = data["body"] + if isinstance(body_data, str): + # Assume it's already base64 encoded JSON string + response.body = body_data + else: + # Convert object to JSON and base64 encode + response.set_body(body_data, "application/json") + if "state" in data: + try: + response.state = State(data["state"]) + except ValueError: + response.state = State.INTERNAL_ERROR + if "metadata" in data: + response.metadata = data["metadata"] + if "redirectUri" in data: + response.redirect_uri = data["redirectUri"] + + return response + except Exception as e: + logger.error("Error deserializing COA response: %s", e) + return COAResponse() + + +__all__ = [ + "ObjectMeta", + "TargetSelector", + "BindingSpec", + "TopologySpec", + "PipelineSpec", + "VersionSpec", + "InstanceSpec", + "FilterSpec", + "RouteSpec", + "ComponentSpec", + "SolutionSpec", + "SolutionState", + "TargetSpec", + "ComponentError", + "TargetError", + "ErrorType", + "ProvisioningStatus", + "TargetStatus", + "TargetState", + "DeviceSpec", + "DeploymentSpec", + "ComparisonPack", + "COABodyMixin", + "COARequest", + "COAResponse", + "to_dict", + "from_dict", + "serialize_components", + "deserialize_components", + "deserialize_solution", + "deserialize_deployment", + "serialize_coa_request", + "deserialize_coa_request", + "serialize_coa_response", + "deserialize_coa_response", +] diff --git a/sdks/symphony-python/src/symphony_sdk/summary.py b/sdks/symphony-python/src/symphony_sdk/summary.py new file mode 100644 index 000000000..529fe4e3b --- /dev/null +++ b/sdks/symphony-python/src/symphony_sdk/summary.py @@ -0,0 +1,334 @@ +"""Symphony API Summary Models - Python Translation + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +SPDX-License-Identifier: MIT + +This module provides Python translations of the Symphony API summary models +from the original Go implementation. +""" + +from dataclasses import dataclass, field +from datetime import datetime +from enum import IntEnum +from typing import Dict + +from symphony_sdk.types import State + + +class SummaryState(IntEnum): + """State enumeration for Symphony summary operations.""" + + PENDING = 0 # Currently unused + RUNNING = 1 # Indicates that a reconcile operation is in progress + DONE = 2 # Indicates that a reconcile operation has completed (successfully or unsuccessfully) + + +@dataclass +class ComponentResultSpec: + """Result specification for a single component operation. + + Attributes: + status: State indicating success/failure of the component operation + message: Optional message with details about the operation + """ + + status: State = State.OK + message: str = "" + + def to_dict(self) -> Dict[str, any]: + """Convert to dictionary representation.""" + return {"status": self.status.value, "message": self.message} + + @classmethod + def from_dict(cls, data: Dict[str, any]) -> "ComponentResultSpec": + """Create instance from dictionary.""" + return cls( + status=State(data.get("status", State.OK.value)), message=data.get("message", "") + ) + + +@dataclass +class TargetResultSpec: + """Result specification for a target containing multiple components. + + Attributes: + status: Overall status string for the target + message: Optional message with target-level details + component_results: Map of component name to component result + """ + + status: str = "OK" + message: str = "" + component_results: Dict[str, ComponentResultSpec] = field(default_factory=dict) + + def to_dict(self) -> Dict[str, any]: + """Convert to dictionary representation.""" + result = {"status": self.status} + if self.message: + result["message"] = self.message + if self.component_results: + result["components"] = { + name: comp_result.to_dict() for name, comp_result in self.component_results.items() + } + return result + + @classmethod + def from_dict(cls, data: Dict[str, any]) -> "TargetResultSpec": + """Create instance from dictionary.""" + component_results = {} + if "components" in data: + component_results = { + name: ComponentResultSpec.from_dict(comp_data) + for name, comp_data in data["components"].items() + } + + return cls( + status=data.get("status", "OK"), + message=data.get("message", ""), + component_results=component_results, + ) + + +@dataclass +class SummarySpec: + """Specification for deployment summary containing target and component results. + + Attributes: + target_count: Total number of targets + success_count: Number of successful deployments + planned_deployment: Number of planned deployments + current_deployed: Number of currently deployed components + target_results: Map of target name to target result + summary_message: Overall summary message + job_id: Optional job identifier + skipped: Whether the deployment was skipped + is_removal: Whether this is a removal operation + all_assigned_deployed: Whether all assigned components are deployed + removed: Whether components were removed + """ + + target_count: int = 0 + success_count: int = 0 + planned_deployment: int = 0 + current_deployed: int = 0 + target_results: Dict[str, TargetResultSpec] = field(default_factory=dict) + summary_message: str = "" + job_id: str = "" + skipped: bool = False + is_removal: bool = False + all_assigned_deployed: bool = False + removed: bool = False + + def update_target_result(self, target: str, spec: TargetResultSpec) -> None: + """Update target result, merging with existing result if present. + + Args: + target: Target name + spec: New target result specification + """ + if target not in self.target_results: + self.target_results[target] = spec + else: + existing = self.target_results[target] + + # Update status - use new status if it's not "OK" + status = existing.status + if spec.status != "OK": + status = spec.status + + # Merge messages + message = existing.message + if spec.message: + if message: + message += "; " + message += spec.message + + # Merge component results + merged_components = existing.component_results.copy() + merged_components.update(spec.component_results) + + # Update the existing result + existing.status = status + existing.message = message + existing.component_results = merged_components + + self.target_results[target] = existing + + def generate_status_message(self) -> str: + """Generate a detailed status message from target and component results. + + Returns: + Formatted status message with error details + """ + if self.all_assigned_deployed: + return "" + + error_message = "Failed to deploy" + if self.summary_message: + error_message += f": {self.summary_message}" + error_message += ". " + + # Get target names and sort them for consistent output + target_names = sorted(self.target_results.keys()) + + # Build target errors in sorted order + target_errors = [] + for target in target_names: + result = self.target_results[target] + target_error = f'{target}: "{result.message}"' + + # Get component names and sort them for consistency + component_names = sorted(result.component_results.keys()) + + # Add component results in sorted order + for component in component_names: + component_result = result.component_results[component] + target_error += f" ({target}.{component}: {component_result.message})" + + target_errors.append(target_error) + + return error_message + f"Detailed status: {', '.join(target_errors)}" + + def to_dict(self) -> Dict[str, any]: + """Convert to dictionary representation.""" + result = { + "targetCount": self.target_count, + "successCount": self.success_count, + "plannedDeployment": self.planned_deployment, + "currentDeployed": self.current_deployed, + "skipped": self.skipped, + "isRemoval": self.is_removal, + "allAssignedDeployed": self.all_assigned_deployed, + "removed": self.removed, + } + + if self.target_results: + result["targets"] = { + name: target_result.to_dict() for name, target_result in self.target_results.items() + } + if self.summary_message: + result["message"] = self.summary_message + if self.job_id: + result["jobID"] = self.job_id + + return result + + @classmethod + def from_dict(cls, data: Dict[str, any]) -> "SummarySpec": + """Create instance from dictionary.""" + target_results = {} + if "targets" in data: + target_results = { + name: TargetResultSpec.from_dict(target_data) + for name, target_data in data["targets"].items() + } + + return cls( + target_count=data.get("targetCount", 0), + success_count=data.get("successCount", 0), + planned_deployment=data.get("plannedDeployment", 0), + current_deployed=data.get("currentDeployed", 0), + target_results=target_results, + summary_message=data.get("message", ""), + job_id=data.get("jobID", ""), + skipped=data.get("skipped", False), + is_removal=data.get("isRemoval", False), + all_assigned_deployed=data.get("allAssignedDeployed", False), + removed=data.get("removed", False), + ) + + +@dataclass +class SummaryResult: + """Complete summary result for a deployment operation. + + Attributes: + summary: The summary specification with all results + summary_id: Optional unique identifier for the summary + generation: Generation string for versioning + time: Timestamp when the summary was created + state: Current state of the summary operation + deployment_hash: Hash of the deployment configuration + """ + + summary: SummarySpec = field(default_factory=SummarySpec) + summary_id: str = "" + generation: str = "" + time: datetime = field(default_factory=datetime.now) + state: SummaryState = SummaryState.PENDING + deployment_hash: str = "" + + def is_deployment_finished(self) -> bool: + """Check if the deployment operation has finished. + + Returns: + True if deployment is done (successfully or unsuccessfully) + """ + return self.state == SummaryState.DONE + + def to_dict(self) -> Dict[str, any]: + """Convert to dictionary representation.""" + return { + "summary": self.summary.to_dict(), + "summaryid": self.summary_id, + "generation": self.generation, + "time": self.time.isoformat(), + "state": self.state.value, + "deploymentHash": self.deployment_hash, + } + + @classmethod + def from_dict(cls, data: Dict[str, any]) -> "SummaryResult": + """Create instance from dictionary.""" + # Parse time string + time_obj = datetime.now() + if "time" in data: + try: + time_obj = datetime.fromisoformat(data["time"]) + except (ValueError, TypeError): + pass + + return cls( + summary=SummarySpec.from_dict(data.get("summary", {})), + summary_id=data.get("summaryid", ""), + generation=data.get("generation", ""), + time=time_obj, + state=SummaryState(data.get("state", SummaryState.PENDING.value)), + deployment_hash=data.get("deploymentHash", ""), + ) + + +# Helper functions for creating common result types +def create_success_component_result(message: str = "") -> ComponentResultSpec: + """Create a successful component result.""" + return ComponentResultSpec(status=State.OK, message=message) + + +def create_failed_component_result(message: str, status: State = None) -> ComponentResultSpec: + """Create a failed component result.""" + if status is None: + status = State.INTERNAL_ERROR + return ComponentResultSpec(status=status, message=message) + + +def create_target_result( + status: str = "OK", message: str = "", component_results: Dict[str, ComponentResultSpec] = None +) -> TargetResultSpec: + """Create a target result specification.""" + if component_results is None: + component_results = {} + return TargetResultSpec(status=status, message=message, component_results=component_results) + + +# Export commonly used items +__all__ = [ + "SummaryState", + "ComponentResultSpec", + "TargetResultSpec", + "SummarySpec", + "SummaryResult", + "create_success_component_result", + "create_failed_component_result", + "create_target_result", +] diff --git a/sdks/symphony-python/src/symphony_sdk/types.py b/sdks/symphony-python/src/symphony_sdk/types.py new file mode 100644 index 000000000..95462e0a5 --- /dev/null +++ b/sdks/symphony-python/src/symphony_sdk/types.py @@ -0,0 +1,339 @@ +"""Symphony COA API Types - Python Translation + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +SPDX-License-Identifier: MIT + +This module provides Python translations of the Symphony COA API types +from the original Go implementation. +""" + +import abc +from enum import IntEnum +from typing import Protocol + + +class Terminable(Protocol): + """Interface for objects that can be gracefully terminated.""" + + @abc.abstractmethod + async def shutdown(self) -> None: + """Shutdown the object gracefully.""" + pass + + +class State(IntEnum): + """State represents a response state matching Symphony Go types.""" + + # Basic states + NONE = 0 + + # HTTP Success states + OK = 200 # HTTP 200 + ACCEPTED = 202 # HTTP 202 + + # HTTP Client Error states + BAD_REQUEST = 400 # HTTP 400 + UNAUTHORIZED = 401 # HTTP 401 + FORBIDDEN = 403 # HTTP 403 + NOT_FOUND = 404 # HTTP 404 + METHOD_NOT_ALLOWED = 405 # HTTP 405 + CONFLICT = 409 # HTTP 409 + STATUS_UNPROCESSABLE_ENTITY = 422 # HTTP 422 + + # HTTP Server Error states + INTERNAL_ERROR = 500 # HTTP 500 + + # Config errors + BAD_CONFIG = 1000 + MISSING_CONFIG = 1001 + + # API invocation errors + INVALID_ARGUMENT = 2000 + API_REDIRECT = 3030 + + # IO errors + FILE_ACCESS_ERROR = 4000 + + # Serialization errors + SERIALIZATION_ERROR = 5000 + DESERIALIZE_ERROR = 5001 + + # Async requests + DELETE_REQUESTED = 6000 + + # Operation results + UPDATE_FAILED = 8001 + DELETE_FAILED = 8002 + VALIDATE_FAILED = 8003 + UPDATED = 8004 + DELETED = 8005 + + # Workflow status + RUNNING = 9994 + PAUSED = 9995 + DONE = 9996 + DELAYED = 9997 + UNTOUCHED = 9998 + NOT_IMPLEMENTED = 9999 + + # Detailed error codes + INIT_FAILED = 10000 + CREATE_ACTION_CONFIG_FAILED = 10001 + HELM_ACTION_FAILED = 10002 + GET_COMPONENT_SPEC_FAILED = 10003 + CREATE_PROJECTOR_FAILED = 10004 + K8S_REMOVE_SERVICE_FAILED = 10005 + K8S_REMOVE_DEPLOYMENT_FAILED = 10006 + K8S_DEPLOYMENT_FAILED = 10007 + READ_YAML_FAILED = 10008 + APPLY_YAML_FAILED = 10009 + READ_RESOURCE_PROPERTY_FAILED = 10010 + APPLY_RESOURCE_FAILED = 10011 + DELETE_YAML_FAILED = 10012 + DELETE_RESOURCE_FAILED = 10013 + CHECK_RESOURCE_STATUS_FAILED = 10014 + APPLY_SCRIPT_FAILED = 10015 + REMOVE_SCRIPT_FAILED = 10016 + YAML_RESOURCE_PROPERTY_NOT_FOUND = 10017 + GET_HELM_PROPERTY_FAILED = 10018 + HELM_CHART_PULL_FAILED = 10019 + HELM_CHART_LOAD_FAILED = 10020 + HELM_CHART_APPLY_FAILED = 10021 + HELM_CHART_UNINSTALL_FAILED = 10022 + INGRESS_APPLY_FAILED = 10023 + HTTP_NEW_REQUEST_FAILED = 10024 + HTTP_SEND_REQUEST_FAILED = 10025 + HTTP_ERROR_RESPONSE = 10026 + MQTT_PUBLISH_FAILED = 10027 + MQTT_APPLY_FAILED = 10028 + MQTT_APPLY_TIMEOUT = 10029 + CONFIG_MAP_APPLY_FAILED = 10030 + HTTP_BAD_WAIT_STATUS_CODE = 10031 + HTTP_NEW_WAIT_REQUEST_FAILED = 10032 + HTTP_SEND_WAIT_REQUEST_FAILED = 10033 + HTTP_ERROR_WAIT_RESPONSE = 10034 + HTTP_BAD_WAIT_EXPRESSION = 10035 + SCRIPT_EXECUTION_FAILED = 10036 + SCRIPT_RESULT_PARSING_FAILED = 10037 + WAIT_TO_GET_INSTANCES_FAILED = 10038 + WAIT_TO_GET_SITES_FAILED = 10039 + WAIT_TO_GET_CATALOGS_FAILED = 10040 + INVALID_WAIT_OBJECT_TYPE = 10041 + CATALOGS_GET_FAILED = 10042 + INVALID_INSTANCE_CATALOG = 10043 + CREATE_INSTANCE_FROM_CATALOG_FAILED = 10044 + INVALID_SOLUTION_CATALOG = 10045 + CREATE_SOLUTION_FROM_CATALOG_FAILED = 10046 + INVALID_TARGET_CATALOG = 10047 + CREATE_TARGET_FROM_CATALOG_FAILED = 10048 + INVALID_CATALOG_CATALOG = 10049 + CREATE_CATALOG_FROM_CATALOG_FAILED = 10050 + PARENT_OBJECT_MISSING = 10051 + PARENT_OBJECT_CREATE_FAILED = 10052 + MATERIALIZE_BATCH_FAILED = 10053 + DELETE_INSTANCE_FAILED = 10054 + CREATE_INSTANCE_FAILED = 10055 + DEPLOYMENT_NOT_REACHED = 10056 + INVALID_OBJECT_TYPE = 10057 + UNSUPPORTED_ACTION = 10058 + INSTANCE_GET_FAILED = 10059 + TARGET_GET_FAILED = 10060 + DELETE_SOLUTION_FAILED = 10061 + CREATE_SOLUTION_FAILED = 10062 + GET_ARM_DEPLOYMENT_PROPERTY_FAILED = 10071 + ENSURE_ARM_RESOURCE_GROUP_FAILED = 10072 + CREATE_ARM_DEPLOYMENT_FAILED = 10073 + CLEANUP_ARM_DEPLOYMENT_FAILED = 10074 + + # Instance controller errors + SOLUTION_GET_FAILED = 11000 + TARGET_CANDIDATES_NOT_FOUND = 11001 + TARGET_LIST_GET_FAILED = 11002 + OBJECT_INSTANCE_CONVERSION_FAILED = 11003 + TIMED_OUT = 11004 + + # Target controller errors + TARGET_PROPERTY_NOT_FOUND = 12000 + + # Non-transient errors + GET_COMPONENT_PROPS_FAILED = 50000 + + def __str__(self) -> str: + """Return human-readable string representation of the state.""" + state_strings = { + State.OK: "OK", + State.ACCEPTED: "Accepted", + State.BAD_REQUEST: "Bad Request", + State.UNAUTHORIZED: "Unauthorized", + State.FORBIDDEN: "Forbidden", + State.NOT_FOUND: "Not Found", + State.METHOD_NOT_ALLOWED: "Method Not Allowed", + State.CONFLICT: "Conflict", + State.STATUS_UNPROCESSABLE_ENTITY: "Unprocessable Entity", + State.INTERNAL_ERROR: "Internal Error", + State.BAD_CONFIG: "Bad Config", + State.MISSING_CONFIG: "Missing Config", + State.INVALID_ARGUMENT: "Invalid Argument", + State.API_REDIRECT: "API Redirect", + State.FILE_ACCESS_ERROR: "File Access Error", + State.SERIALIZATION_ERROR: "Serialization Error", + State.DESERIALIZE_ERROR: "De-serialization Error", + State.DELETE_REQUESTED: "Delete Requested", + State.UPDATE_FAILED: "Update Failed", + State.DELETE_FAILED: "Delete Failed", + State.VALIDATE_FAILED: "Validate Failed", + State.UPDATED: "Updated", + State.DELETED: "Deleted", + State.RUNNING: "Running", + State.PAUSED: "Paused", + State.DONE: "Done", + State.DELAYED: "Delayed", + State.UNTOUCHED: "Untouched", + State.NOT_IMPLEMENTED: "Not Implemented", + State.INIT_FAILED: "Init Failed", + State.CREATE_ACTION_CONFIG_FAILED: "Create Action Config Failed", + State.HELM_ACTION_FAILED: "Helm Action Failed", + State.GET_COMPONENT_SPEC_FAILED: "Get Component Spec Failed", + State.CREATE_PROJECTOR_FAILED: "Create Projector Failed", + State.K8S_REMOVE_SERVICE_FAILED: "Remove K8s Service Failed", + State.K8S_REMOVE_DEPLOYMENT_FAILED: "Remove K8s Deployment Failed", + State.K8S_DEPLOYMENT_FAILED: "K8s Deployment Failed", + State.READ_YAML_FAILED: "Read Yaml Failed", + State.APPLY_YAML_FAILED: "Apply Yaml Failed", + State.READ_RESOURCE_PROPERTY_FAILED: "Read Resource Property Failed", + State.APPLY_RESOURCE_FAILED: "Apply Resource Failed", + State.DELETE_YAML_FAILED: "Delete Yaml Failed", + State.DELETE_RESOURCE_FAILED: "Delete Resource Failed", + State.CHECK_RESOURCE_STATUS_FAILED: "Check Resource Status Failed", + State.APPLY_SCRIPT_FAILED: "Apply Script Failed", + State.REMOVE_SCRIPT_FAILED: "Remove Script Failed", + State.YAML_RESOURCE_PROPERTY_NOT_FOUND: "Yaml or Resource Property Not Found", + State.GET_HELM_PROPERTY_FAILED: "Get Helm Property Failed", + State.HELM_CHART_PULL_FAILED: "Helm Chart Pull Failed", + State.HELM_CHART_LOAD_FAILED: "Helm Chart Load Failed", + State.HELM_CHART_APPLY_FAILED: "Helm Chart Apply Failed", + State.HELM_CHART_UNINSTALL_FAILED: "Helm Chart Uninstall Failed", + State.INGRESS_APPLY_FAILED: "Ingress Apply Failed", + State.HTTP_NEW_REQUEST_FAILED: "Http New Request Failed", + State.HTTP_SEND_REQUEST_FAILED: "Http Send Request Failed", + State.HTTP_ERROR_RESPONSE: "Http Error Response", + State.MQTT_PUBLISH_FAILED: "Mqtt Publish Failed", + State.MQTT_APPLY_FAILED: "Mqtt Apply Failed", + State.MQTT_APPLY_TIMEOUT: "Mqtt Apply Timeout", + State.CONFIG_MAP_APPLY_FAILED: "ConfigMap Apply Failed", + State.HTTP_BAD_WAIT_STATUS_CODE: "Http Bad Wait Status Code", + State.HTTP_NEW_WAIT_REQUEST_FAILED: "Http New Wait Request Failed", + State.HTTP_SEND_WAIT_REQUEST_FAILED: "Http Send Wait Request Failed", + State.HTTP_ERROR_WAIT_RESPONSE: "Http Error Wait Response", + State.HTTP_BAD_WAIT_EXPRESSION: "Http Bad Wait Expression", + State.SCRIPT_EXECUTION_FAILED: "Script Execution Failed", + State.SCRIPT_RESULT_PARSING_FAILED: "Script Result Parsing Failed", + State.WAIT_TO_GET_INSTANCES_FAILED: "Wait To Get Instances Failed", + State.WAIT_TO_GET_SITES_FAILED: "Wait To Get Sites Failed", + State.WAIT_TO_GET_CATALOGS_FAILED: "Wait To Get Catalogs Failed", + State.INVALID_WAIT_OBJECT_TYPE: "Invalid Wait Object Type", + State.CATALOGS_GET_FAILED: "Get Catalogs Failed", + State.INVALID_INSTANCE_CATALOG: "Invalid Instance Catalog", + State.CREATE_INSTANCE_FROM_CATALOG_FAILED: "Create Instance From Catalog Failed", + State.INVALID_SOLUTION_CATALOG: "Invalid Solution Object in Catalog", + State.CREATE_SOLUTION_FROM_CATALOG_FAILED: "Create Solution Object From Catalog Failed", + State.INVALID_TARGET_CATALOG: "Invalid Target Object in Catalog", + State.CREATE_TARGET_FROM_CATALOG_FAILED: "Create Target Object From Catalog Failed", + State.INVALID_CATALOG_CATALOG: "Invalid Catalog Object in Catalog", + State.CREATE_CATALOG_FROM_CATALOG_FAILED: "Create Catalog Object From Catalog Failed", + State.PARENT_OBJECT_MISSING: "Parent Object Missing", + State.PARENT_OBJECT_CREATE_FAILED: "Parent Object Create Failed", + State.MATERIALIZE_BATCH_FAILED: "Failed to Materialize all objects", + State.DELETE_INSTANCE_FAILED: "Failed to Delete Instance", + State.CREATE_INSTANCE_FAILED: "Failed to Create Instance", + State.DEPLOYMENT_NOT_REACHED: "Deployment Not Reached", + State.INVALID_OBJECT_TYPE: "Invalid Object Type", + State.UNSUPPORTED_ACTION: "Unsupported Action", + State.INSTANCE_GET_FAILED: "Get instance failed", + State.TARGET_GET_FAILED: "Get target failed", + State.SOLUTION_GET_FAILED: "Solution does not exist", + State.TARGET_CANDIDATES_NOT_FOUND: "Target does not exist", + State.TARGET_LIST_GET_FAILED: "Target list does not exist", + State.OBJECT_INSTANCE_CONVERSION_FAILED: "Object to Instance conversion failed", + State.TIMED_OUT: "Timed Out", + State.TARGET_PROPERTY_NOT_FOUND: "Target Property Not Found", + State.GET_COMPONENT_PROPS_FAILED: "Get component property failed", + } + + return state_strings.get(self, f"Unknown State: {self.value}") + + def equals_with_string(self, string: str) -> bool: + """Check if state equals a string representation.""" + return str(self) == string + + @classmethod + def from_http_status(cls, code: int) -> "State": + """Get State from HTTP status code.""" + if code == 200: + return cls.OK + elif code == 202: + return cls.ACCEPTED + elif 200 <= code < 300: + return cls.OK + elif code == 401: + return cls.UNAUTHORIZED + elif code == 403: + return cls.FORBIDDEN + elif code == 404: + return cls.NOT_FOUND + elif code == 405: + return cls.METHOD_NOT_ALLOWED + elif code == 409: + return cls.CONFLICT + elif 400 <= code < 500: + return cls.BAD_REQUEST + elif code >= 500: + return cls.INTERNAL_ERROR + else: + return cls.NONE + + +# COA Constants +class COAConstants: + """Constants used in Symphony COA operations.""" + + # Header constants + COA_META_HEADER = "COA_META_HEADER" + + # Tracing and monitoring + TRACING_EXPORTER_CONSOLE = "tracing.exporters.console" + METRICS_EXPORTER_OTLP_GRPC = "metrics.exporters.otlpgrpc" + TRACING_EXPORTER_ZIPKIN = "tracing.exporters.zipkin" + TRACING_EXPORTER_OTLP_GRPC = "tracing.exporters.otlpgrpc" + LOG_EXPORTER_CONSOLE = "log.exporters.console" + LOG_EXPORTER_OTLP_GRPC = "log.exporters.otlpgrpc" + LOG_EXPORTER_OTLP_HTTP = "log.exporters.otlphttp" + + # Provider constants + PROVIDERS_PERSISTENT_STATE = "providers.persistentstate" + PROVIDERS_VOLATILE_STATE = "providers.volatilestate" + PROVIDERS_CONFIG = "providers.config" + PROVIDERS_SECRET = "providers.secret" + PROVIDERS_REFERENCE = "providers.reference" + PROVIDERS_PROBE = "providers.probe" + PROVIDERS_UPLOADER = "providers.uploader" + PROVIDERS_REPORTER = "providers.reporter" + PROVIDER_QUEUE = "providers.queue" + PROVIDER_LEDGER = "providers.ledger" + PROVIDERS_KEY_LOCK = "providers.keylock" + + # Output constants + STATUS_OUTPUT = "status" + ERROR_OUTPUT = "error" + STATE_OUTPUT = "__state" + + +# Helper functions for compatibility +def get_http_status(code: int) -> State: + """Get State from HTTP status code (compatibility function).""" + return State.from_http_status(code) + + +# Export commonly used items +__all__ = ["State", "Terminable", "COAConstants", "get_http_status"] diff --git a/sdks/symphony-python/tests/test_api_client.py b/sdks/symphony-python/tests/test_api_client.py new file mode 100644 index 000000000..effb325f3 --- /dev/null +++ b/sdks/symphony-python/tests/test_api_client.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +""" +Simplified unit tests for Symphony API client focusing on actual methods. +""" + +import unittest +from unittest.mock import Mock, patch + +import requests + +from symphony_sdk.api_client import SymphonyAPI, SymphonyAPIError + + +class TestSymphonyAPIError(unittest.TestCase): + """Test cases for SymphonyAPIError exception.""" + + def test_symphony_api_error_creation(self): + """Test SymphonyAPIError creation.""" + error = SymphonyAPIError("Test error message") + self.assertEqual(str(error), "Test error message") + self.assertIsNone(error.status_code) + self.assertIsNone(error.response_text) + + def test_symphony_api_error_with_details(self): + """Test SymphonyAPIError creation with details.""" + error = SymphonyAPIError("API request failed", status_code=404, response_text="Not Found") + self.assertEqual(str(error), "API request failed") + self.assertEqual(error.status_code, 404) + + +class TestSymphonyAPI(unittest.TestCase): + """Test cases for the SymphonyAPI REST client.""" + + def setUp(self): + """Set up test fixtures.""" + self.base_url = "https://symphony.example.com" + self.username = "testuser" + self.password = "testpass" + self.client = SymphonyAPI( + base_url=self.base_url, username=self.username, password=self.password, timeout=10.0 + ) + + def tearDown(self): + """Clean up after tests.""" + if self.client: + self.client.close() + + def test_client_initialization(self): + """Test SymphonyAPI initialization.""" + self.assertEqual(self.client.base_url, self.base_url) + self.assertEqual(self.client.username, self.username) + self.assertEqual(self.client.password, self.password) + self.assertEqual(self.client.timeout, 10.0) + + def test_client_context_manager(self): + """Test SymphonyAPI as context manager.""" + with SymphonyAPI(self.base_url, self.username, self.password) as client: + self.assertIsInstance(client, SymphonyAPI) + # Client should be closed after context exit + + @patch("requests.Session.request") + def test_make_request_basic_functionality(self, mock_request): + """Test basic request functionality.""" + # Mock successful response + mock_response = Mock() + mock_response.status_code = 200 + mock_response.text = "OK" + mock_request.return_value = mock_response + + response = self.client._make_request("GET", "/api/test") + + self.assertEqual(response.status_code, 200) + self.assertTrue(mock_request.called) + + @patch("requests.Session.request") + def test_make_request_timeout_handling(self, mock_request): + """Test request timeout handling.""" + mock_request.side_effect = requests.exceptions.Timeout("Request timed out") + + with self.assertRaises(SymphonyAPIError) as context: + self.client._make_request("GET", "/api/test") + + self.assertIn("request failed", str(context.exception).lower()) + + @patch.object(SymphonyAPI, "_handle_response") + @patch.object(SymphonyAPI, "_make_request") + def test_authenticate_basic(self, mock_make_request, mock_handle_response): + """Test basic authentication functionality.""" + # Mock successful auth response with correct key name + mock_response = Mock() + mock_make_request.return_value = mock_response + mock_handle_response.return_value = {"accessToken": "test_token"} + + token = self.client.authenticate() + + self.assertEqual(token, "test_token") + self.assertEqual(self.client._access_token, "test_token") + + @patch.object(SymphonyAPI, "_handle_response") + @patch.object(SymphonyAPI, "_make_request") + def test_register_target_basic(self, mock_make_request, mock_handle_response): + """Test basic target registration.""" + # Set up authentication + self.client._access_token = "test_token" + + mock_response = Mock() + mock_make_request.return_value = mock_response + mock_handle_response.return_value = {"status": "registered"} + + target_name = "test-target" + target_spec = {"type": "device", "location": "datacenter1"} + + result = self.client.register_target(target_name, target_spec) + + self.assertEqual(result, {"status": "registered"}) + self.assertTrue(mock_make_request.called) + + @patch.object(SymphonyAPI, "_handle_response") + @patch.object(SymphonyAPI, "_make_request") + def test_get_target_basic(self, mock_make_request, mock_handle_response): + """Test basic target retrieval.""" + # Set up authentication + self.client._access_token = "test_token" + + mock_response = Mock() + mock_make_request.return_value = mock_response + mock_handle_response.return_value = { + "name": "test-target", + "status": "active", + "spec": {"type": "device"}, + } + + target_name = "test-target" + result = self.client.get_target(target_name) + + self.assertEqual(result["name"], target_name) + self.assertEqual(result["status"], "active") + + @patch.object(SymphonyAPI, "_handle_response") + @patch.object(SymphonyAPI, "_make_request") + def test_list_targets_basic(self, mock_make_request, mock_handle_response): + """Test basic target listing.""" + # Set up authentication + self.client._access_token = "test_token" + + mock_response = Mock() + mock_make_request.return_value = mock_response + mock_handle_response.return_value = { + "targets": [ + {"name": "target1", "status": "active"}, + {"name": "target2", "status": "inactive"}, + ] + } + + result = self.client.list_targets() + + self.assertIn("targets", result) + self.assertEqual(len(result["targets"]), 2) + + @patch.object(SymphonyAPI, "_handle_response") + @patch.object(SymphonyAPI, "_make_request") + def test_unregister_target_basic(self, mock_make_request, mock_handle_response): + """Test basic target unregistration.""" + # Set up authentication + self.client._access_token = "test_token" + + mock_response = Mock() + mock_make_request.return_value = mock_response + mock_handle_response.return_value = {"status": "unregistered"} + + target_name = "test-target" + + result = self.client.unregister_target(target_name) + + self.assertEqual(result, {"status": "unregistered"}) + + +if __name__ == "__main__": + unittest.main() diff --git a/sdks/symphony-python/tests/test_models.py b/sdks/symphony-python/tests/test_models.py new file mode 100644 index 000000000..7e676bc64 --- /dev/null +++ b/sdks/symphony-python/tests/test_models.py @@ -0,0 +1,450 @@ +#!/usr/bin/env python3 +""" +Comprehensive unit tests for Symphony SDK core functionality. +""" + +import base64 +import json +import unittest + +from symphony_sdk.models import ( + COARequest, + COAResponse, + ComponentSpec, + DeploymentSpec, + ObjectMeta, + SolutionSpec, + SolutionState, + deserialize_coa_request, + deserialize_coa_response, + deserialize_components, + deserialize_deployment, + serialize_coa_request, + serialize_coa_response, + serialize_components, + to_dict, +) +from symphony_sdk.types import State + + +class TestObjectMeta(unittest.TestCase): + """Test cases for ObjectMeta dataclass.""" + + def test_object_meta_creation(self): + """Test ObjectMeta creation with default values.""" + meta = ObjectMeta() + self.assertEqual(meta.name, "") + self.assertEqual(meta.namespace, "") + self.assertIsNone(meta.labels) + self.assertIsNone(meta.annotations) + + def test_object_meta_with_values(self): + """Test ObjectMeta creation with specific values.""" + labels = {"app": "test", "version": "1.0"} + annotations = {"description": "test object"} + meta = ObjectMeta( + name="test-object", namespace="test-namespace", labels=labels, annotations=annotations + ) + self.assertEqual(meta.name, "test-object") + self.assertEqual(meta.namespace, "test-namespace") + self.assertEqual(meta.labels, labels) + self.assertEqual(meta.annotations, annotations) + + +class TestComponentSpec(unittest.TestCase): + """Test cases for ComponentSpec dataclass.""" + + def test_component_spec_creation(self): + """Test ComponentSpec creation with defaults.""" + comp = ComponentSpec() + self.assertEqual(comp.name, "") + self.assertEqual(comp.type, "") + self.assertIsNone(comp.routes) + self.assertEqual(comp.constraints, "") + self.assertIsNone(comp.properties) + + def test_component_spec_with_values(self): + """Test ComponentSpec creation with specific values.""" + properties = {"key1": "value1", "key2": "value2"} + comp = ComponentSpec( + name="test-component", type="service", constraints="cpu=2", properties=properties + ) + + self.assertEqual(comp.name, "test-component") + self.assertEqual(comp.type, "service") + self.assertEqual(comp.constraints, "cpu=2") + self.assertEqual(comp.properties, properties) + + +class TestDeploymentSpec(unittest.TestCase): + """Test cases for DeploymentSpec dataclass.""" + + def test_deployment_spec_creation(self): + """Test DeploymentSpec creation with defaults.""" + deployment = DeploymentSpec() + self.assertEqual(deployment.solutionName, "") + self.assertIsNone(deployment.solution) + self.assertIsNone(deployment.instance) + self.assertEqual(deployment.activeTarget, "") + + def test_deployment_spec_get_components_slice_no_solution(self): + """Test get_components_slice with no solution.""" + deployment = DeploymentSpec() + components = deployment.get_components_slice() + self.assertEqual(components, []) + + def test_deployment_spec_get_components_slice_with_indices(self): + """Test get_components_slice with start and end indices.""" + # Create components + comp1 = ComponentSpec(name="comp1") + comp2 = ComponentSpec(name="comp2") + comp3 = ComponentSpec(name="comp3") + + # Create solution with components + solution_spec = SolutionSpec(components=[comp1, comp2, comp3]) + solution_state = SolutionState(spec=solution_spec) + + deployment = DeploymentSpec( + solution=solution_state, componentStartIndex=1, componentEndIndex=3 + ) + + components = deployment.get_components_slice() + self.assertEqual(len(components), 2) + self.assertEqual(components[0].name, "comp2") + self.assertEqual(components[1].name, "comp3") + + def test_deployment_spec_get_components_slice_all_components(self): + """Test get_components_slice returning all components.""" + comp1 = ComponentSpec(name="comp1") + comp2 = ComponentSpec(name="comp2") + + solution_spec = SolutionSpec(components=[comp1, comp2]) + solution_state = SolutionState(spec=solution_spec) + + deployment = DeploymentSpec(solution=solution_state) + + components = deployment.get_components_slice() + self.assertEqual(len(components), 2) + self.assertEqual(components[0].name, "comp1") + self.assertEqual(components[1].name, "comp2") + + +class TestCOABodyMixin(unittest.TestCase): + """Test cases for COABodyMixin functionality.""" + + def test_coa_request_set_json_body(self): + """Test COARequest set_body with JSON data.""" + request = COARequest() + data = {"key": "value", "number": 123} + + request.set_body(data, "application/json") + + self.assertEqual(request.content_type, "application/json") + + # Decode and verify + decoded_body = json.loads(base64.b64decode(request.body).decode("utf-8")) + self.assertEqual(decoded_body, data) + + def test_coa_request_set_text_body(self): + """Test COARequest set_body with text data.""" + request = COARequest() + text_data = "Hello, World!" + + request.set_body(text_data, "text/plain") + + self.assertEqual(request.content_type, "text/plain") + self.assertEqual(request.body, text_data) + + def test_coa_request_set_binary_body(self): + """Test COARequest set_body with binary data.""" + request = COARequest() + binary_data = b"Binary data content" + + request.set_body(binary_data, "application/octet-stream") + + self.assertEqual(request.content_type, "application/octet-stream") + + # Decode and verify + decoded_body = base64.b64decode(request.body) + self.assertEqual(decoded_body, binary_data) + + def test_coa_request_get_json_body(self): + """Test COARequest get_body with JSON data.""" + request = COARequest() + data = {"test": "data", "array": [1, 2, 3]} + + request.set_body(data, "application/json") + retrieved_data = request.get_body() + + self.assertEqual(retrieved_data, data) + + def test_coa_request_get_text_body(self): + """Test COARequest get_body with text data.""" + request = COARequest() + text_data = "Simple text content" + + request.set_body(text_data, "text/plain") + retrieved_data = request.get_body() + + self.assertEqual(retrieved_data, text_data) + + def test_coa_request_get_binary_body(self): + """Test COARequest get_body with binary data.""" + request = COARequest() + binary_data = b"Binary content for testing" + + request.set_body(binary_data, "application/octet-stream") + retrieved_data = request.get_body() + + self.assertEqual(retrieved_data, binary_data) + + +class TestCOARequest(unittest.TestCase): + """Test cases for COARequest dataclass.""" + + def test_coa_request_creation(self): + """Test COARequest creation with defaults.""" + request = COARequest() + self.assertEqual(request.method, "GET") + self.assertEqual(request.route, "") + self.assertEqual(request.content_type, "application/json") + self.assertEqual(request.body, "") + + def test_coa_request_with_values(self): + """Test COARequest creation with specific values.""" + metadata = {"version": "1.0"} + parameters = {"param1": "value1"} + + request = COARequest( + method="POST", route="/api/v1/deploy", metadata=metadata, parameters=parameters + ) + + self.assertEqual(request.method, "POST") + self.assertEqual(request.route, "/api/v1/deploy") + self.assertEqual(request.metadata, metadata) + self.assertEqual(request.parameters, parameters) + + def test_coa_request_to_json_dict(self): + """Test COARequest to_json_dict conversion.""" + request = COARequest( + method="PUT", route="/test", metadata={"key": "value"}, parameters={"param": "test"} + ) + request.set_body({"data": "test"}) + + json_dict = request.to_json_dict() + + expected_keys = ["method", "route", "content-type", "body", "metadata", "parameters"] + for key in expected_keys: + self.assertIn(key, json_dict) + + self.assertEqual(json_dict["method"], "PUT") + self.assertEqual(json_dict["route"], "/test") + self.assertEqual(json_dict["content-type"], "application/json") + + +class TestCOAResponse(unittest.TestCase): + """Test cases for COAResponse dataclass.""" + + def test_coa_response_creation(self): + """Test COAResponse creation with defaults.""" + response = COAResponse() + self.assertEqual(response.state, State.OK) + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.body, "") + + def test_coa_response_success_factory(self): + """Test COAResponse.success factory method.""" + data = {"result": "success", "count": 5} + response = COAResponse.success(data) + + self.assertEqual(response.state, State.OK) + self.assertEqual(response.content_type, "application/json") + + retrieved_data = response.get_body() + self.assertEqual(retrieved_data, data) + + def test_coa_response_error_factory(self): + """Test COAResponse.error factory method.""" + error_msg = "Something went wrong" + response = COAResponse.error(error_msg, State.INTERNAL_ERROR) + + self.assertEqual(response.state, State.INTERNAL_ERROR) + retrieved_data = response.get_body() + self.assertEqual(retrieved_data["error"], error_msg) + + def test_coa_response_not_found_factory(self): + """Test COAResponse.not_found factory method.""" + response = COAResponse.not_found("Resource not found") + + self.assertEqual(response.state, State.NOT_FOUND) + retrieved_data = response.get_body() + self.assertEqual(retrieved_data["error"], "Resource not found") + + def test_coa_response_bad_request_factory(self): + """Test COAResponse.bad_request factory method.""" + response = COAResponse.bad_request("Invalid input") + + self.assertEqual(response.state, State.BAD_REQUEST) + retrieved_data = response.get_body() + self.assertEqual(retrieved_data["error"], "Invalid input") + + def test_coa_response_to_json_dict(self): + """Test COAResponse to_json_dict conversion.""" + response = COAResponse( + state=State.OK, metadata={"version": "1.0"}, redirect_uri="https://example.com/redirect" + ) + response.set_body({"status": "ok"}) + + json_dict = response.to_json_dict() + + expected_keys = ["content-type", "body", "state", "metadata", "redirectUri"] + for key in expected_keys: + self.assertIn(key, json_dict) + + self.assertEqual(json_dict["state"], State.OK.value) + self.assertEqual(json_dict["redirectUri"], "https://example.com/redirect") + + +class TestUtilityFunctions(unittest.TestCase): + """Test cases for utility functions.""" + + def test_to_dict_with_simple_object(self): + """Test to_dict with simple dataclass object.""" + comp = ComponentSpec(name="test", type="service") + result = to_dict(comp) + + self.assertIsInstance(result, dict) + self.assertEqual(result["name"], "test") + self.assertEqual(result["type"], "service") + + def test_to_dict_with_none(self): + """Test to_dict with None input.""" + result = to_dict(None) + self.assertEqual(result, {}) + + def test_to_dict_with_nested_objects(self): + """Test to_dict with nested dataclass objects.""" + meta = ObjectMeta(name="test-meta") + comp = ComponentSpec(name="test-comp") + solution_spec = SolutionSpec(components=[comp]) + solution_state = SolutionState(metadata=meta, spec=solution_spec) + + result = to_dict(solution_state) + + self.assertIsInstance(result, dict) + self.assertIn("metadata", result) + self.assertIn("spec", result) + self.assertEqual(result["metadata"]["name"], "test-meta") + + def test_serialize_components(self): + """Test serialize_components function.""" + comp1 = ComponentSpec(name="comp1", type="service") + comp2 = ComponentSpec(name="comp2", type="deployment") + components = [comp1, comp2] + + json_str = serialize_components(components) + + self.assertIsInstance(json_str, str) + + # Verify JSON is valid + data = json.loads(json_str) + self.assertEqual(len(data), 2) + self.assertEqual(data[0]["name"], "comp1") + self.assertEqual(data[1]["name"], "comp2") + + def test_deserialize_components(self): + """Test deserialize_components function.""" + json_data = [ + {"name": "test-comp1", "type": "service"}, + {"name": "test-comp2", "type": "deployment"}, + ] + json_str = json.dumps(json_data) + + components = deserialize_components(json_str) + + self.assertEqual(len(components), 2) + self.assertEqual(components[0].name, "test-comp1") + self.assertEqual(components[0].type, "service") + self.assertEqual(components[1].name, "test-comp2") + self.assertEqual(components[1].type, "deployment") + + def test_deserialize_components_invalid_json(self): + """Test deserialize_components with invalid JSON.""" + invalid_json = "{ invalid json }" + components = deserialize_components(invalid_json) + self.assertEqual(components, []) + + def test_deserialize_deployment(self): + """Test deserialize_deployment function.""" + deployment_data = {"solutionName": "test-solution", "activeTarget": "test-target"} + json_str = json.dumps(deployment_data) + + deployments = deserialize_deployment(json_str) + + self.assertEqual(len(deployments), 1) + self.assertEqual(deployments[0].solutionName, "test-solution") + self.assertEqual(deployments[0].activeTarget, "test-target") + + def test_serialize_coa_request(self): + """Test serialize_coa_request function.""" + request = COARequest(method="POST", route="/test", metadata={"key": "value"}) + request.set_body({"data": "test"}) + + json_str = serialize_coa_request(request) + + self.assertIsInstance(json_str, str) + + # Verify JSON is valid + data = json.loads(json_str) + self.assertEqual(data["method"], "POST") + self.assertEqual(data["route"], "/test") + + def test_deserialize_coa_request(self): + """Test deserialize_coa_request function.""" + request_data = { + "method": "PUT", + "route": "/api/test", + "content-type": "application/json", + "body": base64.b64encode(json.dumps({"test": "data"}).encode()).decode(), + "metadata": {"version": "1.0"}, + } + json_str = json.dumps(request_data) + + request = deserialize_coa_request(json_str) + + self.assertEqual(request.method, "PUT") + self.assertEqual(request.route, "/api/test") + self.assertEqual(request.content_type, "application/json") + self.assertEqual(request.metadata, {"version": "1.0"}) + + def test_serialize_coa_response(self): + """Test serialize_coa_response function.""" + response = COAResponse.success({"result": "ok"}) + + json_str = serialize_coa_response(response) + + self.assertIsInstance(json_str, str) + + # Verify JSON is valid + data = json.loads(json_str) + self.assertEqual(data["state"], State.OK.value) + + def test_deserialize_coa_response(self): + """Test deserialize_coa_response function.""" + response_data = { + "content-type": "application/json", + "body": base64.b64encode(json.dumps({"status": "success"}).encode()).decode(), + "state": State.ACCEPTED.value, + "metadata": {"processed": "true"}, + } + json_str = json.dumps(response_data) + + response = deserialize_coa_response(json_str) + + self.assertEqual(response.state, State.ACCEPTED) + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.metadata, {"processed": "true"}) + + +if __name__ == "__main__": + unittest.main() diff --git a/sdks/symphony-python/tests/test_summary.py b/sdks/symphony-python/tests/test_summary.py new file mode 100644 index 000000000..f5ffcc367 --- /dev/null +++ b/sdks/symphony-python/tests/test_summary.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 +""" +Comprehensive unit tests for Symphony SDK summary models. +""" + +import unittest + +from symphony_sdk.summary import ( + ComponentResultSpec, + SummaryResult, + SummarySpec, + SummaryState, + TargetResultSpec, + create_failed_component_result, + create_success_component_result, + create_target_result, +) +from symphony_sdk.types import State + + +class TestSummaryState(unittest.TestCase): + """Test cases for SummaryState enumeration.""" + + def test_summary_state_values(self): + """Test SummaryState enum values.""" + self.assertEqual(SummaryState.PENDING, 0) + self.assertEqual(SummaryState.RUNNING, 1) + self.assertEqual(SummaryState.DONE, 2) + + def test_summary_state_from_int(self): + """Test creating SummaryState from integer.""" + self.assertEqual(SummaryState(0), SummaryState.PENDING) + self.assertEqual(SummaryState(1), SummaryState.RUNNING) + self.assertEqual(SummaryState(2), SummaryState.DONE) + + +class TestComponentResultSpec(unittest.TestCase): + """Test cases for ComponentResultSpec.""" + + def test_component_result_spec_creation(self): + """Test ComponentResultSpec creation with default values.""" + result = ComponentResultSpec() + self.assertEqual(result.status, State.OK) + self.assertEqual(result.message, "") + + def test_component_result_spec_with_values(self): + """Test ComponentResultSpec creation with specific values.""" + result = ComponentResultSpec(status=State.UPDATE_FAILED, message="Update operation failed") + self.assertEqual(result.status, State.UPDATE_FAILED) + self.assertEqual(result.message, "Update operation failed") + + def test_component_result_spec_to_dict(self): + """Test ComponentResultSpec to_dict conversion.""" + result = ComponentResultSpec(status=State.UPDATED, message="Component updated successfully") + + expected_dict = {"status": State.UPDATED.value, "message": "Component updated successfully"} + + self.assertEqual(result.to_dict(), expected_dict) + + def test_component_result_spec_from_dict(self): + """Test ComponentResultSpec from_dict conversion.""" + data = {"status": State.DELETE_FAILED.value, "message": "Failed to delete component"} + + result = ComponentResultSpec.from_dict(data) + self.assertEqual(result.status, State.DELETE_FAILED) + self.assertEqual(result.message, "Failed to delete component") + + def test_component_result_spec_from_dict_defaults(self): + """Test ComponentResultSpec from_dict with missing values.""" + data = {} + result = ComponentResultSpec.from_dict(data) + self.assertEqual(result.status, State.OK) + self.assertEqual(result.message, "") + + def test_component_result_spec_from_dict_partial(self): + """Test ComponentResultSpec from_dict with partial data.""" + data = {"status": State.UPDATED.value} + result = ComponentResultSpec.from_dict(data) + self.assertEqual(result.status, State.UPDATED) + self.assertEqual(result.message, "") + + +class TestTargetResultSpec(unittest.TestCase): + """Test cases for TargetResultSpec.""" + + def test_target_result_spec_creation(self): + """Test TargetResultSpec creation with defaults.""" + result = TargetResultSpec() + self.assertEqual(result.status, "OK") + self.assertEqual(result.message, "") + self.assertEqual(result.component_results, {}) + + def test_target_result_spec_with_values(self): + """Test TargetResultSpec creation with specific values.""" + comp1 = ComponentResultSpec(State.UPDATED, "Component 1 updated") + comp2 = ComponentResultSpec(State.UPDATE_FAILED, "Component 2 failed") + + result = TargetResultSpec( + status="PARTIAL_SUCCESS", + message="Some components failed", + component_results={"comp1": comp1, "comp2": comp2}, + ) + + self.assertEqual(result.status, "PARTIAL_SUCCESS") + self.assertEqual(result.message, "Some components failed") + self.assertEqual(len(result.component_results), 2) + self.assertEqual(result.component_results["comp1"], comp1) + self.assertEqual(result.component_results["comp2"], comp2) + + def test_target_result_spec_to_dict(self): + """Test TargetResultSpec to_dict conversion.""" + comp_result = ComponentResultSpec(State.UPDATED, "Success") + result = TargetResultSpec( + status="SUCCESS", + message="All components updated", + component_results={"comp1": comp_result}, + ) + + result_dict = result.to_dict() + + expected_dict = { + "status": "SUCCESS", + "message": "All components updated", + "components": {"comp1": {"status": State.UPDATED.value, "message": "Success"}}, + } + + self.assertEqual(result_dict, expected_dict) + + def test_target_result_spec_to_dict_minimal(self): + """Test TargetResultSpec to_dict with minimal data.""" + result = TargetResultSpec(status="OK") + result_dict = result.to_dict() + + expected_dict = {"status": "OK"} + self.assertEqual(result_dict, expected_dict) + + def test_target_result_spec_from_dict(self): + """Test TargetResultSpec from_dict conversion.""" + data = { + "status": "FAILED", + "message": "Operation failed", + "components": { + "comp1": {"status": State.UPDATE_FAILED.value, "message": "Component update failed"} + }, + } + + result = TargetResultSpec.from_dict(data) + + self.assertEqual(result.status, "FAILED") + self.assertEqual(result.message, "Operation failed") + self.assertEqual(len(result.component_results), 1) + + comp_result = result.component_results["comp1"] + self.assertEqual(comp_result.status, State.UPDATE_FAILED) + self.assertEqual(comp_result.message, "Component update failed") + + def test_target_result_spec_from_dict_no_components(self): + """Test TargetResultSpec from_dict without components.""" + data = {"status": "SUCCESS", "message": "No components to process"} + + result = TargetResultSpec.from_dict(data) + self.assertEqual(result.status, "SUCCESS") + self.assertEqual(result.message, "No components to process") + self.assertEqual(result.component_results, {}) + + +class TestSummaryResult(unittest.TestCase): + """Test cases for SummaryResult.""" + + def test_summary_result_creation(self): + """Test SummaryResult creation.""" + summary_spec = SummarySpec() + summary = SummaryResult(summary=summary_spec, state=SummaryState.DONE, generation="1") + + self.assertEqual(summary.state, SummaryState.DONE) + self.assertEqual(summary.generation, "1") + self.assertEqual(summary.summary, summary_spec) + + def test_summary_result_is_deployment_finished(self): + """Test SummaryResult is_deployment_finished method.""" + # Test with DONE state + summary_done = SummaryResult(state=SummaryState.DONE) + self.assertTrue(summary_done.is_deployment_finished()) + + # Test with RUNNING state + summary_running = SummaryResult(state=SummaryState.RUNNING) + self.assertFalse(summary_running.is_deployment_finished()) + + +class TestSummarySpec(unittest.TestCase): + """Test cases for SummarySpec.""" + + def test_summary_spec_creation(self): + """Test SummarySpec creation.""" + spec = SummarySpec(target_count=2, success_count=1) + + self.assertEqual(spec.target_count, 2) + self.assertEqual(spec.success_count, 1) + + def test_summary_spec_defaults(self): + """Test SummarySpec with default values.""" + spec = SummarySpec() + self.assertEqual(spec.target_count, 0) + self.assertEqual(spec.success_count, 0) + + +class TestHelperFunctions(unittest.TestCase): + """Test cases for helper functions.""" + + def test_create_success_component_result(self): + """Test create_success_component_result function.""" + result = create_success_component_result("Operation completed") + + self.assertEqual(result.status, State.OK) + self.assertEqual(result.message, "Operation completed") + + def test_create_success_component_result_default_message(self): + """Test create_success_component_result with default message.""" + result = create_success_component_result() + + self.assertEqual(result.status, State.OK) + self.assertEqual(result.message, "") + + def test_create_failed_component_result(self): + """Test create_failed_component_result function.""" + result = create_failed_component_result("Operation failed", State.UPDATE_FAILED) + + self.assertEqual(result.status, State.UPDATE_FAILED) + self.assertEqual(result.message, "Operation failed") + + def test_create_failed_component_result_default_state(self): + """Test create_failed_component_result with default state.""" + result = create_failed_component_result("Something went wrong") + + self.assertEqual(result.status, State.INTERNAL_ERROR) + self.assertEqual(result.message, "Something went wrong") + + def test_create_target_result(self): + """Test create_target_result function.""" + components = { + "comp1": create_success_component_result("Updated"), + "comp2": create_failed_component_result("Failed"), + } + + result = create_target_result("PARTIAL", "Some components failed", components) + + self.assertEqual(result.status, "PARTIAL") + self.assertEqual(result.message, "Some components failed") + self.assertEqual(len(result.component_results), 2) + self.assertEqual(result.component_results["comp1"].status, State.OK) + self.assertEqual(result.component_results["comp2"].status, State.INTERNAL_ERROR) + + def test_create_target_result_minimal(self): + """Test create_target_result with minimal parameters.""" + result = create_target_result() + + self.assertEqual(result.status, "OK") + self.assertEqual(result.message, "") + self.assertEqual(result.component_results, {}) + + +if __name__ == "__main__": + unittest.main() diff --git a/sdks/symphony-python/tests/test_types.py b/sdks/symphony-python/tests/test_types.py new file mode 100644 index 000000000..1dbc6ff40 --- /dev/null +++ b/sdks/symphony-python/tests/test_types.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +""" +Comprehensive unit tests for Symphony SDK types and enumerations. +""" + +import asyncio +import unittest + +from symphony_sdk.types import COAConstants, State, get_http_status + + +class MockTerminable: + """Mock implementation of Terminable for testing.""" + + def __init__(self): + self.shutdown_called = False + + async def shutdown(self) -> None: + """Mock shutdown method.""" + self.shutdown_called = True + + +class TestState(unittest.TestCase): + """Test cases for State enumeration.""" + + def test_state_values(self): + """Test that State enum has correct values.""" + # Basic states + self.assertEqual(State.NONE, 0) + + # HTTP Success states + self.assertEqual(State.OK, 200) + self.assertEqual(State.ACCEPTED, 202) + + # HTTP Client Error states + self.assertEqual(State.BAD_REQUEST, 400) + self.assertEqual(State.UNAUTHORIZED, 401) + self.assertEqual(State.FORBIDDEN, 403) + self.assertEqual(State.NOT_FOUND, 404) + self.assertEqual(State.METHOD_NOT_ALLOWED, 405) + self.assertEqual(State.CONFLICT, 409) + self.assertEqual(State.STATUS_UNPROCESSABLE_ENTITY, 422) + + # HTTP Server Error states + self.assertEqual(State.INTERNAL_ERROR, 500) + + # Config errors + self.assertEqual(State.BAD_CONFIG, 1000) + self.assertEqual(State.MISSING_CONFIG, 1001) + + # Operation results + self.assertEqual(State.UPDATE_FAILED, 8001) + self.assertEqual(State.DELETE_FAILED, 8002) + self.assertEqual(State.VALIDATE_FAILED, 8003) + self.assertEqual(State.UPDATED, 8004) + self.assertEqual(State.DELETED, 8005) + + # Workflow status + self.assertEqual(State.RUNNING, 9994) + self.assertEqual(State.PAUSED, 9995) + self.assertEqual(State.DONE, 9996) + self.assertEqual(State.DELAYED, 9997) + self.assertEqual(State.UNTOUCHED, 9998) + self.assertEqual(State.NOT_IMPLEMENTED, 9999) + + def test_state_http_success_codes(self): + """Test HTTP success state codes.""" + success_states = [State.OK, State.ACCEPTED] + for state in success_states: + self.assertGreaterEqual(state, 200) + self.assertLess(state, 300) + + def test_state_http_client_error_codes(self): + """Test HTTP client error state codes.""" + client_error_states = [ + State.BAD_REQUEST, + State.UNAUTHORIZED, + State.FORBIDDEN, + State.NOT_FOUND, + State.METHOD_NOT_ALLOWED, + State.CONFLICT, + State.STATUS_UNPROCESSABLE_ENTITY, + ] + for state in client_error_states: + self.assertGreaterEqual(state, 400) + self.assertLess(state, 500) + + def test_state_http_server_error_codes(self): + """Test HTTP server error state codes.""" + server_error_states = [State.INTERNAL_ERROR] + for state in server_error_states: + self.assertGreaterEqual(state, 500) + self.assertLess(state, 600) + + def test_state_custom_error_codes(self): + """Test custom Symphony error codes.""" + custom_states = [ + State.BAD_CONFIG, + State.MISSING_CONFIG, + State.INVALID_ARGUMENT, + State.API_REDIRECT, + State.FILE_ACCESS_ERROR, + State.SERIALIZATION_ERROR, + State.DESERIALIZE_ERROR, + State.DELETE_REQUESTED, + ] + for state in custom_states: + self.assertGreater(state, 999) # All custom states > 999 + + def test_state_from_int(self): + """Test creating State from integer values.""" + self.assertEqual(State(200), State.OK) + self.assertEqual(State(404), State.NOT_FOUND) + self.assertEqual(State(500), State.INTERNAL_ERROR) + + def test_state_int_conversion(self): + """Test converting State to integer.""" + self.assertEqual(int(State.OK), 200) + self.assertEqual(int(State.NOT_FOUND), 404) + self.assertEqual(int(State.INTERNAL_ERROR), 500) + + +class TestTerminable(unittest.TestCase): + """Test cases for Terminable protocol.""" + + def test_terminable_protocol(self): + """Test that objects implement Terminable protocol correctly.""" + mock_terminable = MockTerminable() + + # Should have shutdown method + self.assertTrue(hasattr(mock_terminable, "shutdown")) + self.assertTrue(callable(mock_terminable.shutdown)) + + def test_terminable_shutdown(self): + """Test Terminable shutdown behavior.""" + + async def run_test(): + mock_terminable = MockTerminable() + self.assertFalse(mock_terminable.shutdown_called) + + await mock_terminable.shutdown() + + self.assertTrue(mock_terminable.shutdown_called) + + # Run the async test + asyncio.run(run_test()) + + +class TestHttpStatusFunction(unittest.TestCase): + """Test cases for get_http_status function.""" + + def test_get_http_status_success_codes(self): + """Test get_http_status with success codes.""" + self.assertEqual(get_http_status(200), State.OK) + self.assertEqual(get_http_status(202), State.ACCEPTED) + + def test_get_http_status_client_error_codes(self): + """Test get_http_status with client error codes.""" + self.assertEqual(get_http_status(400), State.BAD_REQUEST) + self.assertEqual(get_http_status(401), State.UNAUTHORIZED) + self.assertEqual(get_http_status(404), State.NOT_FOUND) + + def test_get_http_status_server_error_codes(self): + """Test get_http_status with server error codes.""" + self.assertEqual(get_http_status(500), State.INTERNAL_ERROR) + + def test_get_http_status_custom_codes(self): + """Test get_http_status with custom HTTP codes.""" + # Test other success codes + self.assertEqual(get_http_status(201), State.OK) + self.assertEqual(get_http_status(204), State.OK) + + # Test other client error codes + self.assertEqual(get_http_status(422), State.BAD_REQUEST) + + # Test other server error codes + self.assertEqual(get_http_status(503), State.INTERNAL_ERROR) + + +class TestCOAConstants(unittest.TestCase): + """Test cases for COA constants.""" + + def test_coa_constants_exist(self): + """Test that COA constants are defined.""" + # Test some expected constants exist + self.assertTrue(hasattr(COAConstants, "COA_META_HEADER")) + self.assertTrue(hasattr(COAConstants, "TRACING_EXPORTER_CONSOLE")) + self.assertTrue(hasattr(COAConstants, "PROVIDERS_CONFIG")) + + # Test values are strings + self.assertEqual(COAConstants.COA_META_HEADER, "COA_META_HEADER") + self.assertEqual(COAConstants.PROVIDERS_CONFIG, "providers.config") + + def test_coa_constants_types(self): + """Test that COA constants have correct types.""" + self.assertIsInstance(COAConstants.COA_META_HEADER, str) + self.assertIsInstance(COAConstants.TRACING_EXPORTER_CONSOLE, str) + + +if __name__ == "__main__": + unittest.main() diff --git a/sdks/symphony-python/uv.lock b/sdks/symphony-python/uv.lock new file mode 100644 index 000000000..bce5538ac --- /dev/null +++ b/sdks/symphony-python/uv.lock @@ -0,0 +1,1330 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" }, + { url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", size = 203243, upload-time = "2025-09-08T23:23:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", size = 203158, upload-time = "2025-09-08T23:23:55.169Z" }, + { url = "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" }, + { url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" }, + { url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041, upload-time = "2025-09-08T23:24:00.496Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/46/7c/0c4760bccf082737ca7ab84a4c2034fcc06b1f21cf3032ea98bd6feb1725/charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", size = 209609, upload-time = "2025-10-14T04:42:10.922Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a4/69719daef2f3d7f1819de60c9a6be981b8eeead7542d5ec4440f3c80e111/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", size = 149029, upload-time = "2025-10-14T04:42:12.38Z" }, + { url = "https://files.pythonhosted.org/packages/e6/21/8d4e1d6c1e6070d3672908b8e4533a71b5b53e71d16828cc24d0efec564c/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608", size = 144580, upload-time = "2025-10-14T04:42:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/a7/0a/a616d001b3f25647a9068e0b9199f697ce507ec898cacb06a0d5a1617c99/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", size = 162340, upload-time = "2025-10-14T04:42:14.892Z" }, + { url = "https://files.pythonhosted.org/packages/85/93/060b52deb249a5450460e0585c88a904a83aec474ab8e7aba787f45e79f2/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", size = 159619, upload-time = "2025-10-14T04:42:16.676Z" }, + { url = "https://files.pythonhosted.org/packages/dd/21/0274deb1cc0632cd587a9a0ec6b4674d9108e461cb4cd40d457adaeb0564/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", size = 153980, upload-time = "2025-10-14T04:42:17.917Z" }, + { url = "https://files.pythonhosted.org/packages/28/2b/e3d7d982858dccc11b31906976323d790dded2017a0572f093ff982d692f/charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", size = 152174, upload-time = "2025-10-14T04:42:19.018Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ff/4a269f8e35f1e58b2df52c131a1fa019acb7ef3f8697b7d464b07e9b492d/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", size = 151666, upload-time = "2025-10-14T04:42:20.171Z" }, + { url = "https://files.pythonhosted.org/packages/da/c9/ec39870f0b330d58486001dd8e532c6b9a905f5765f58a6f8204926b4a93/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", size = 145550, upload-time = "2025-10-14T04:42:21.324Z" }, + { url = "https://files.pythonhosted.org/packages/75/8f/d186ab99e40e0ed9f82f033d6e49001701c81244d01905dd4a6924191a30/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", size = 163721, upload-time = "2025-10-14T04:42:22.46Z" }, + { url = "https://files.pythonhosted.org/packages/96/b1/6047663b9744df26a7e479ac1e77af7134b1fcf9026243bb48ee2d18810f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", size = 152127, upload-time = "2025-10-14T04:42:23.712Z" }, + { url = "https://files.pythonhosted.org/packages/59/78/e5a6eac9179f24f704d1be67d08704c3c6ab9f00963963524be27c18ed87/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", size = 161175, upload-time = "2025-10-14T04:42:24.87Z" }, + { url = "https://files.pythonhosted.org/packages/e5/43/0e626e42d54dd2f8dd6fc5e1c5ff00f05fbca17cb699bedead2cae69c62f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", size = 155375, upload-time = "2025-10-14T04:42:27.246Z" }, + { url = "https://files.pythonhosted.org/packages/e9/91/d9615bf2e06f35e4997616ff31248c3657ed649c5ab9d35ea12fce54e380/charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", size = 99692, upload-time = "2025-10-14T04:42:28.425Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a9/6c040053909d9d1ef4fcab45fddec083aedc9052c10078339b47c8573ea8/charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", size = 107192, upload-time = "2025-10-14T04:42:29.482Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c6/4fa536b2c0cd3edfb7ccf8469fa0f363ea67b7213a842b90909ca33dd851/charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", size = 100220, upload-time = "2025-10-14T04:42:30.632Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[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 = "coverage" +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, + { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, + { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, + { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, + { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" }, + { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" }, + { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" }, + { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" }, + { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version < '3.10'" }, +] + +[[package]] +name = "coverage" +version = "7.13.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, + { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" }, + { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" }, + { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" }, + { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" }, + { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" }, + { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, + { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, + { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, + { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, + { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, + { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, + { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, + { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, + { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, + { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, + { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, + { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, + { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, + { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, + { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, + { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, + { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, + { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, + { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, + { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, + { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, + { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, + { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "id" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/11/102da08f88412d875fa2f1a9a469ff7ad4c874b0ca6fed0048fe385bdb3d/id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", size = 15237, upload-time = "2024-12-04T19:53:05.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/cb/18326d2d89ad3b0dd143da971e77afd1e6ca6674f1b1c3df4b6bec6279fc/id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658", size = 13611, upload-time = "2024-12-04T19:53:03.02Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +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 = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[package]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", version = "3.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and sys_platform == 'linux'" }, + { name = "secretstorage", version = "3.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + +[[package]] +name = "librt" +version = "0.7.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/29/47f29026ca17f35cf299290292d5f8331f5077364974b7675a353179afa2/librt-0.7.7.tar.gz", hash = "sha256:81d957b069fed1890953c3b9c3895c7689960f233eea9a1d9607f71ce7f00b2c", size = 145910, upload-time = "2026-01-01T23:52:22.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/84/2cfb1f3b9b60bab52e16a220c931223fc8e963d0d7bb9132bef012aafc3f/librt-0.7.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4836c5645f40fbdc275e5670819bde5ab5f2e882290d304e3c6ddab1576a6d0", size = 54709, upload-time = "2026-01-01T23:50:48.326Z" }, + { url = "https://files.pythonhosted.org/packages/19/a1/3127b277e9d3784a8040a54e8396d9ae5c64d6684dc6db4b4089b0eedcfb/librt-0.7.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae8aec43117a645a31e5f60e9e3a0797492e747823b9bda6972d521b436b4e8", size = 56658, upload-time = "2026-01-01T23:50:49.74Z" }, + { url = "https://files.pythonhosted.org/packages/3a/e9/b91b093a5c42eb218120445f3fef82e0b977fa2225f4d6fc133d25cdf86a/librt-0.7.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:aea05f701ccd2a76b34f0daf47ca5068176ff553510b614770c90d76ac88df06", size = 161026, upload-time = "2026-01-01T23:50:50.853Z" }, + { url = "https://files.pythonhosted.org/packages/c7/cb/1ded77d5976a79d7057af4a010d577ce4f473ff280984e68f4974a3281e5/librt-0.7.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b16ccaeff0ed4355dfb76fe1ea7a5d6d03b5ad27f295f77ee0557bc20a72495", size = 169529, upload-time = "2026-01-01T23:50:52.24Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/6ca5bdaa701e15f05000ac1a4c5d1475c422d3484bd3d1ca9e8c2f5be167/librt-0.7.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c48c7e150c095d5e3cea7452347ba26094be905d6099d24f9319a8b475fcd3e0", size = 183271, upload-time = "2026-01-01T23:50:55.287Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2d/55c0e38073997b4bbb5ddff25b6d1bbba8c2f76f50afe5bb9c844b702f34/librt-0.7.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4dcee2f921a8632636d1c37f1bbdb8841d15666d119aa61e5399c5268e7ce02e", size = 179039, upload-time = "2026-01-01T23:50:56.807Z" }, + { url = "https://files.pythonhosted.org/packages/33/4e/3662a41ae8bb81b226f3968426293517b271d34d4e9fd4b59fc511f1ae40/librt-0.7.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:14ef0f4ac3728ffd85bfc58e2f2f48fb4ef4fa871876f13a73a7381d10a9f77c", size = 173505, upload-time = "2026-01-01T23:50:58.291Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5d/cf768deb8bdcbac5f8c21fcb32dd483d038d88c529fd351bbe50590b945d/librt-0.7.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e4ab69fa37f8090f2d971a5d2bc606c7401170dbdae083c393d6cbf439cb45b8", size = 193570, upload-time = "2026-01-01T23:50:59.546Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ea/ee70effd13f1d651976d83a2812391f6203971740705e3c0900db75d4bce/librt-0.7.7-cp310-cp310-win32.whl", hash = "sha256:4bf3cc46d553693382d2abf5f5bd493d71bb0f50a7c0beab18aa13a5545c8900", size = 42600, upload-time = "2026-01-01T23:51:00.694Z" }, + { url = "https://files.pythonhosted.org/packages/f0/eb/dc098730f281cba76c279b71783f5de2edcba3b880c1ab84a093ef826062/librt-0.7.7-cp310-cp310-win_amd64.whl", hash = "sha256:f0c8fe5aeadd8a0e5b0598f8a6ee3533135ca50fd3f20f130f9d72baf5c6ac58", size = 48977, upload-time = "2026-01-01T23:51:01.726Z" }, + { url = "https://files.pythonhosted.org/packages/f0/56/30b5c342518005546df78841cb0820ae85a17e7d07d521c10ef367306d0d/librt-0.7.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a487b71fbf8a9edb72a8c7a456dda0184642d99cd007bc819c0b7ab93676a8ee", size = 54709, upload-time = "2026-01-01T23:51:02.774Z" }, + { url = "https://files.pythonhosted.org/packages/72/78/9f120e3920b22504d4f3835e28b55acc2cc47c9586d2e1b6ba04c3c1bf01/librt-0.7.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f4d4efb218264ecf0f8516196c9e2d1a0679d9fb3bb15df1155a35220062eba8", size = 56663, upload-time = "2026-01-01T23:51:03.838Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ea/7d7a1ee7dfc1151836028eba25629afcf45b56bbc721293e41aa2e9b8934/librt-0.7.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b8bb331aad734b059c4b450cd0a225652f16889e286b2345af5e2c3c625c3d85", size = 161705, upload-time = "2026-01-01T23:51:04.917Z" }, + { url = "https://files.pythonhosted.org/packages/45/a5/952bc840ac8917fbcefd6bc5f51ad02b89721729814f3e2bfcc1337a76d6/librt-0.7.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:467dbd7443bda08338fc8ad701ed38cef48194017554f4c798b0a237904b3f99", size = 171029, upload-time = "2026-01-01T23:51:06.09Z" }, + { url = "https://files.pythonhosted.org/packages/fa/bf/c017ff7da82dc9192cf40d5e802a48a25d00e7639b6465cfdcee5893a22c/librt-0.7.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50d1d1ee813d2d1a3baf2873634ba506b263032418d16287c92ec1cc9c1a00cb", size = 184704, upload-time = "2026-01-01T23:51:07.549Z" }, + { url = "https://files.pythonhosted.org/packages/77/ec/72f3dd39d2cdfd6402ab10836dc9cbf854d145226062a185b419c4f1624a/librt-0.7.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e5070cf3ec92d98f57574da0224f8c73faf1ddd6d8afa0b8c9f6e86997bc74", size = 180719, upload-time = "2026-01-01T23:51:09.062Z" }, + { url = "https://files.pythonhosted.org/packages/78/86/06e7a1a81b246f3313bf515dd9613a1c81583e6fd7843a9f4d625c4e926d/librt-0.7.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bdb9f3d865b2dafe7f9ad7f30ef563c80d0ddd2fdc8cc9b8e4f242f475e34d75", size = 174537, upload-time = "2026-01-01T23:51:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/83/08/f9fb2edc9c7a76e95b2924ce81d545673f5b034e8c5dd92159d1c7dae0c6/librt-0.7.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8185c8497d45164e256376f9da5aed2bb26ff636c798c9dabe313b90e9f25b28", size = 195238, upload-time = "2026-01-01T23:51:11.762Z" }, + { url = "https://files.pythonhosted.org/packages/ba/56/ea2d2489d3ea1f47b301120e03a099e22de7b32c93df9a211e6ff4f9bf38/librt-0.7.7-cp311-cp311-win32.whl", hash = "sha256:44d63ce643f34a903f09ff7ca355aae019a3730c7afd6a3c037d569beeb5d151", size = 42939, upload-time = "2026-01-01T23:51:13.192Z" }, + { url = "https://files.pythonhosted.org/packages/58/7b/c288f417e42ba2a037f1c0753219e277b33090ed4f72f292fb6fe175db4c/librt-0.7.7-cp311-cp311-win_amd64.whl", hash = "sha256:7d13cc340b3b82134f8038a2bfe7137093693dcad8ba5773da18f95ad6b77a8a", size = 49240, upload-time = "2026-01-01T23:51:14.264Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/738eb33a6c1516fdb2dfd2a35db6e5300f7616679b573585be0409bc6890/librt-0.7.7-cp311-cp311-win_arm64.whl", hash = "sha256:983de36b5a83fe9222f4f7dcd071f9b1ac6f3f17c0af0238dadfb8229588f890", size = 42613, upload-time = "2026-01-01T23:51:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/56/72/1cd9d752070011641e8aee046c851912d5f196ecd726fffa7aed2070f3e0/librt-0.7.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a85a1fc4ed11ea0eb0a632459ce004a2d14afc085a50ae3463cd3dfe1ce43fc", size = 55687, upload-time = "2026-01-01T23:51:16.291Z" }, + { url = "https://files.pythonhosted.org/packages/50/aa/d5a1d4221c4fe7e76ae1459d24d6037783cb83c7645164c07d7daf1576ec/librt-0.7.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c87654e29a35938baead1c4559858f346f4a2a7588574a14d784f300ffba0efd", size = 57136, upload-time = "2026-01-01T23:51:17.363Z" }, + { url = "https://files.pythonhosted.org/packages/23/6f/0c86b5cb5e7ef63208c8cc22534df10ecc5278efc0d47fb8815577f3ca2f/librt-0.7.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c9faaebb1c6212c20afd8043cd6ed9de0a47d77f91a6b5b48f4e46ed470703fe", size = 165320, upload-time = "2026-01-01T23:51:18.455Z" }, + { url = "https://files.pythonhosted.org/packages/16/37/df4652690c29f645ffe405b58285a4109e9fe855c5bb56e817e3e75840b3/librt-0.7.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1908c3e5a5ef86b23391448b47759298f87f997c3bd153a770828f58c2bb4630", size = 174216, upload-time = "2026-01-01T23:51:19.599Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d6/d3afe071910a43133ec9c0f3e4ce99ee6df0d4e44e4bddf4b9e1c6ed41cc/librt-0.7.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbc4900e95a98fc0729523be9d93a8fedebb026f32ed9ffc08acd82e3e181503", size = 189005, upload-time = "2026-01-01T23:51:21.052Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/74060a870fe2d9fd9f47824eba6717ce7ce03124a0d1e85498e0e7efc1b2/librt-0.7.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a7ea4e1fbd253e5c68ea0fe63d08577f9d288a73f17d82f652ebc61fa48d878d", size = 183961, upload-time = "2026-01-01T23:51:22.493Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5e/918a86c66304af66a3c1d46d54df1b2d0b8894babc42a14fb6f25511497f/librt-0.7.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef7699b7a5a244b1119f85c5bbc13f152cd38240cbb2baa19b769433bae98e50", size = 177610, upload-time = "2026-01-01T23:51:23.874Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d7/b5e58dc2d570f162e99201b8c0151acf40a03a39c32ab824dd4febf12736/librt-0.7.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:955c62571de0b181d9e9e0a0303c8bc90d47670a5eff54cf71bf5da61d1899cf", size = 199272, upload-time = "2026-01-01T23:51:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/87/8202c9bd0968bdddc188ec3811985f47f58ed161b3749299f2c0dd0f63fb/librt-0.7.7-cp312-cp312-win32.whl", hash = "sha256:1bcd79be209313b270b0e1a51c67ae1af28adad0e0c7e84c3ad4b5cb57aaa75b", size = 43189, upload-time = "2026-01-01T23:51:26.799Z" }, + { url = "https://files.pythonhosted.org/packages/61/8d/80244b267b585e7aa79ffdac19f66c4861effc3a24598e77909ecdd0850e/librt-0.7.7-cp312-cp312-win_amd64.whl", hash = "sha256:4353ee891a1834567e0302d4bd5e60f531912179578c36f3d0430f8c5e16b456", size = 49462, upload-time = "2026-01-01T23:51:27.813Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1f/75db802d6a4992d95e8a889682601af9b49d5a13bbfa246d414eede1b56c/librt-0.7.7-cp312-cp312-win_arm64.whl", hash = "sha256:a76f1d679beccccdf8c1958e732a1dfcd6e749f8821ee59d7bec009ac308c029", size = 42828, upload-time = "2026-01-01T23:51:28.804Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5e/d979ccb0a81407ec47c14ea68fb217ff4315521730033e1dd9faa4f3e2c1/librt-0.7.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4a0b0a3c86ba9193a8e23bb18f100d647bf192390ae195d84dfa0a10fb6244", size = 55746, upload-time = "2026-01-01T23:51:29.828Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/3b65861fb32f802c3783d6ac66fc5589564d07452a47a8cf9980d531cad3/librt-0.7.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5335890fea9f9e6c4fdf8683061b9ccdcbe47c6dc03ab8e9b68c10acf78be78d", size = 57174, upload-time = "2026-01-01T23:51:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/50/df/030b50614b29e443607220097ebaf438531ea218c7a9a3e21ea862a919cd/librt-0.7.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b4346b1225be26def3ccc6c965751c74868f0578cbcba293c8ae9168483d811", size = 165834, upload-time = "2026-01-01T23:51:32.278Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e1/bd8d1eacacb24be26a47f157719553bbd1b3fe812c30dddf121c0436fd0b/librt-0.7.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a10b8eebdaca6e9fdbaf88b5aefc0e324b763a5f40b1266532590d5afb268a4c", size = 174819, upload-time = "2026-01-01T23:51:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/91d6c3372acf54a019c1ad8da4c9ecf4fc27d039708880bf95f48dbe426a/librt-0.7.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:067be973d90d9e319e6eb4ee2a9b9307f0ecd648b8a9002fa237289a4a07a9e7", size = 189607, upload-time = "2026-01-01T23:51:34.604Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ac/44604d6d3886f791fbd1c6ae12d5a782a8f4aca927484731979f5e92c200/librt-0.7.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23d2299ed007812cccc1ecef018db7d922733382561230de1f3954db28433977", size = 184586, upload-time = "2026-01-01T23:51:35.845Z" }, + { url = "https://files.pythonhosted.org/packages/5c/26/d8a6e4c17117b7f9b83301319d9a9de862ae56b133efb4bad8b3aa0808c9/librt-0.7.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6b6f8ea465524aa4c7420c7cc4ca7d46fe00981de8debc67b1cc2e9957bb5b9d", size = 178251, upload-time = "2026-01-01T23:51:37.018Z" }, + { url = "https://files.pythonhosted.org/packages/99/ab/98d857e254376f8e2f668e807daccc1f445e4b4fc2f6f9c1cc08866b0227/librt-0.7.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8df32a99cc46eb0ee90afd9ada113ae2cafe7e8d673686cf03ec53e49635439", size = 199853, upload-time = "2026-01-01T23:51:38.195Z" }, + { url = "https://files.pythonhosted.org/packages/7c/55/4523210d6ae5134a5da959900be43ad8bab2e4206687b6620befddb5b5fd/librt-0.7.7-cp313-cp313-win32.whl", hash = "sha256:86f86b3b785487c7760247bcdac0b11aa8bf13245a13ed05206286135877564b", size = 43247, upload-time = "2026-01-01T23:51:39.629Z" }, + { url = "https://files.pythonhosted.org/packages/25/40/3ec0fed5e8e9297b1cf1a3836fb589d3de55f9930e3aba988d379e8ef67c/librt-0.7.7-cp313-cp313-win_amd64.whl", hash = "sha256:4862cb2c702b1f905c0503b72d9d4daf65a7fdf5a9e84560e563471e57a56949", size = 49419, upload-time = "2026-01-01T23:51:40.674Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7a/aab5f0fb122822e2acbc776addf8b9abfb4944a9056c00c393e46e543177/librt-0.7.7-cp313-cp313-win_arm64.whl", hash = "sha256:0996c83b1cb43c00e8c87835a284f9057bc647abd42b5871e5f941d30010c832", size = 42828, upload-time = "2026-01-01T23:51:41.731Z" }, + { url = "https://files.pythonhosted.org/packages/69/9c/228a5c1224bd23809a635490a162e9cbdc68d99f0eeb4a696f07886b8206/librt-0.7.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:23daa1ab0512bafdd677eb1bfc9611d8ffbe2e328895671e64cb34166bc1b8c8", size = 55188, upload-time = "2026-01-01T23:51:43.14Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c2/0e7c6067e2b32a156308205e5728f4ed6478c501947e9142f525afbc6bd2/librt-0.7.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:558a9e5a6f3cc1e20b3168fb1dc802d0d8fa40731f6e9932dcc52bbcfbd37111", size = 56895, upload-time = "2026-01-01T23:51:44.534Z" }, + { url = "https://files.pythonhosted.org/packages/0e/77/de50ff70c80855eb79d1d74035ef06f664dd073fb7fb9d9fb4429651b8eb/librt-0.7.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2567cb48dc03e5b246927ab35cbb343376e24501260a9b5e30b8e255dca0d1d2", size = 163724, upload-time = "2026-01-01T23:51:45.571Z" }, + { url = "https://files.pythonhosted.org/packages/6e/19/f8e4bf537899bdef9e0bb9f0e4b18912c2d0f858ad02091b6019864c9a6d/librt-0.7.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6066c638cdf85ff92fc6f932d2d73c93a0e03492cdfa8778e6d58c489a3d7259", size = 172470, upload-time = "2026-01-01T23:51:46.823Z" }, + { url = "https://files.pythonhosted.org/packages/42/4c/dcc575b69d99076768e8dd6141d9aecd4234cba7f0e09217937f52edb6ed/librt-0.7.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a609849aca463074c17de9cda173c276eb8fee9e441053529e7b9e249dc8b8ee", size = 186806, upload-time = "2026-01-01T23:51:48.009Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f8/4094a2b7816c88de81239a83ede6e87f1138477d7ee956c30f136009eb29/librt-0.7.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:add4e0a000858fe9bb39ed55f31085506a5c38363e6eb4a1e5943a10c2bfc3d1", size = 181809, upload-time = "2026-01-01T23:51:49.35Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ac/821b7c0ab1b5a6cd9aee7ace8309c91545a2607185101827f79122219a7e/librt-0.7.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a3bfe73a32bd0bdb9a87d586b05a23c0a1729205d79df66dee65bb2e40d671ba", size = 175597, upload-time = "2026-01-01T23:51:50.636Z" }, + { url = "https://files.pythonhosted.org/packages/71/f9/27f6bfbcc764805864c04211c6ed636fe1d58f57a7b68d1f4ae5ed74e0e0/librt-0.7.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0ecce0544d3db91a40f8b57ae26928c02130a997b540f908cefd4d279d6c5848", size = 196506, upload-time = "2026-01-01T23:51:52.535Z" }, + { url = "https://files.pythonhosted.org/packages/46/ba/c9b9c6fc931dd7ea856c573174ccaf48714905b1a7499904db2552e3bbaf/librt-0.7.7-cp314-cp314-win32.whl", hash = "sha256:8f7a74cf3a80f0c3b0ec75b0c650b2f0a894a2cec57ef75f6f72c1e82cdac61d", size = 39747, upload-time = "2026-01-01T23:51:53.683Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/cd1269337c4cde3ee70176ee611ab0058aa42fc8ce5c9dce55f48facfcd8/librt-0.7.7-cp314-cp314-win_amd64.whl", hash = "sha256:3d1fe2e8df3268dd6734dba33ededae72ad5c3a859b9577bc00b715759c5aaab", size = 45971, upload-time = "2026-01-01T23:51:54.697Z" }, + { url = "https://files.pythonhosted.org/packages/79/fd/e0844794423f5583108c5991313c15e2b400995f44f6ec6871f8aaf8243c/librt-0.7.7-cp314-cp314-win_arm64.whl", hash = "sha256:2987cf827011907d3dfd109f1be0d61e173d68b1270107bb0e89f2fca7f2ed6b", size = 39075, upload-time = "2026-01-01T23:51:55.726Z" }, + { url = "https://files.pythonhosted.org/packages/42/02/211fd8f7c381e7b2a11d0fdfcd410f409e89967be2e705983f7c6342209a/librt-0.7.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8e92c8de62b40bfce91d5e12c6e8b15434da268979b1af1a6589463549d491e6", size = 57368, upload-time = "2026-01-01T23:51:56.706Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/aca257affae73ece26041ae76032153266d110453173f67d7603058e708c/librt-0.7.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f683dcd49e2494a7535e30f779aa1ad6e3732a019d80abe1309ea91ccd3230e3", size = 59238, upload-time = "2026-01-01T23:51:58.066Z" }, + { url = "https://files.pythonhosted.org/packages/96/47/7383a507d8e0c11c78ca34c9d36eab9000db5989d446a2f05dc40e76c64f/librt-0.7.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b15e5d17812d4d629ff576699954f74e2cc24a02a4fc401882dd94f81daba45", size = 183870, upload-time = "2026-01-01T23:51:59.204Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/50f3d8eec8efdaf79443963624175c92cec0ba84827a66b7fcfa78598e51/librt-0.7.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c084841b879c4d9b9fa34e5d5263994f21aea7fd9c6add29194dbb41a6210536", size = 194608, upload-time = "2026-01-01T23:52:00.419Z" }, + { url = "https://files.pythonhosted.org/packages/23/d9/1b6520793aadb59d891e3b98ee057a75de7f737e4a8b4b37fdbecb10d60f/librt-0.7.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c8fb9966f84737115513fecbaf257f9553d067a7dd45a69c2c7e5339e6a8dc", size = 206776, upload-time = "2026-01-01T23:52:01.705Z" }, + { url = "https://files.pythonhosted.org/packages/ff/db/331edc3bba929d2756fa335bfcf736f36eff4efcb4f2600b545a35c2ae58/librt-0.7.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5fb1ecb2c35362eab2dbd354fd1efa5a8440d3e73a68be11921042a0edc0ff", size = 203206, upload-time = "2026-01-01T23:52:03.315Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e1/6af79ec77204e85f6f2294fc171a30a91bb0e35d78493532ed680f5d98be/librt-0.7.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d1454899909d63cc9199a89fcc4f81bdd9004aef577d4ffc022e600c412d57f3", size = 196697, upload-time = "2026-01-01T23:52:04.857Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/de55ecce4b2796d6d243295c221082ca3a944dc2fb3a52dcc8660ce7727d/librt-0.7.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7ef28f2e7a016b29792fe0a2dd04dec75725b32a1264e390c366103f834a9c3a", size = 217193, upload-time = "2026-01-01T23:52:06.159Z" }, + { url = "https://files.pythonhosted.org/packages/41/61/33063e271949787a2f8dd33c5260357e3d512a114fc82ca7890b65a76e2d/librt-0.7.7-cp314-cp314t-win32.whl", hash = "sha256:5e419e0db70991b6ba037b70c1d5bbe92b20ddf82f31ad01d77a347ed9781398", size = 40277, upload-time = "2026-01-01T23:52:07.625Z" }, + { url = "https://files.pythonhosted.org/packages/06/21/1abd972349f83a696ea73159ac964e63e2d14086fdd9bc7ca878c25fced4/librt-0.7.7-cp314-cp314t-win_amd64.whl", hash = "sha256:d6b7d93657332c817b8d674ef6bf1ab7796b4f7ce05e420fd45bd258a72ac804", size = 46765, upload-time = "2026-01-01T23:52:08.647Z" }, + { url = "https://files.pythonhosted.org/packages/51/0e/b756c7708143a63fca65a51ca07990fa647db2cc8fcd65177b9e96680255/librt-0.7.7-cp314-cp314t-win_arm64.whl", hash = "sha256:142c2cd91794b79fd0ce113bd658993b7ede0fe93057668c2f98a45ca00b7e91", size = 39724, upload-time = "2026-01-01T23:52:09.745Z" }, + { url = "https://files.pythonhosted.org/packages/e2/34/b88347b7bac496c1433e2f9bf124b0024733654b1bb4bcbf6ccf24d83e2e/librt-0.7.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c8ffe3431d98cc043a14e88b21288b5ec7ee12cb01260e94385887f285ef9389", size = 54841, upload-time = "2026-01-01T23:52:10.751Z" }, + { url = "https://files.pythonhosted.org/packages/01/fc/394ef13f4a9a407e43e76a8b0002042f53e22401014ee19544bab99ba2c9/librt-0.7.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e40d20ae1722d6b8ea6acf4597e789604649dcd9c295eb7361a28225bc2e9e12", size = 56804, upload-time = "2026-01-01T23:52:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/88/53/0d49f17dd11495f0274d34318bd5d1c1aa183ce97c45a2dce8fda9b650af/librt-0.7.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f2cb63c49bc96847c3bb8dca350970e4dcd19936f391cfdfd057dcb37c4fa97e", size = 159682, upload-time = "2026-01-01T23:52:13.34Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/cce20900af63bbc22abacb197622287cf210cfdf2da352131fa48c3e490e/librt-0.7.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f2f8dcf5ab9f80fb970c6fd780b398efb2f50c1962485eb8d3ab07788595a48", size = 168512, upload-time = "2026-01-01T23:52:14.52Z" }, + { url = "https://files.pythonhosted.org/packages/18/aa/4d5e0e98b47998297ec58e14561346f38bc4ad2d7c4d100e0a3baead06e8/librt-0.7.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1f5cc41a570269d1be7a676655875e3a53de4992a9fa38efb7983e97cf73d7c", size = 182231, upload-time = "2026-01-01T23:52:15.656Z" }, + { url = "https://files.pythonhosted.org/packages/d7/76/6dbde6632fd959f4ffb1b9a6ee67ae096adce6222282c7b9cd131787ea16/librt-0.7.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ff1fb2dfef035549565a4124998fadcb7a3d4957131ddf004a56edeb029626b3", size = 178268, upload-time = "2026-01-01T23:52:16.851Z" }, + { url = "https://files.pythonhosted.org/packages/83/7d/a3ce1a98fa5a79c87e8d24a6595ba5beff40f500051d933f771975b81df9/librt-0.7.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ab2a2a9cd7d044e1a11ca64a86ad3361d318176924bbe5152fbc69f99be20b8c", size = 172569, upload-time = "2026-01-01T23:52:18.033Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a7/01f6cbc77b0ccb22d9ad939ddcd1529a521d3e79c5b1eb3ed5b2c158e8dd/librt-0.7.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ad3fc2d859a709baf9dd9607bb72f599b1cfb8a39eafd41307d0c3c4766763cb", size = 192746, upload-time = "2026-01-01T23:52:19.235Z" }, + { url = "https://files.pythonhosted.org/packages/5b/83/9da96065a4f5a44eb1b7e6611c729544b84bb5dd6806acbf0c82ba3e3c27/librt-0.7.7-cp39-cp39-win32.whl", hash = "sha256:f83c971eb9d2358b6a18da51dc0ae00556ac7c73104dde16e9e14c15aaf685ca", size = 42550, upload-time = "2026-01-01T23:52:20.392Z" }, + { url = "https://files.pythonhosted.org/packages/34/b3/85aef151a052a40521f5b54005908a22c437dd4c952800d5e5efce99a47d/librt-0.7.7-cp39-cp39-win_amd64.whl", hash = "sha256:264720fc288c86039c091a4ad63419a5d7cabbf1c1c9933336a957ed2483e570", size = 48957, upload-time = "2026-01-01T23:52:21.43Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[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 = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/88436084550ca9af5e610fa45286be04c3b63374df3e021c762fe8c4369f/mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3", size = 13102606, upload-time = "2025-12-15T05:02:46.833Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a5/43dfad311a734b48a752790571fd9e12d61893849a01bff346a54011957f/mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a", size = 12164496, upload-time = "2025-12-15T05:03:41.947Z" }, + { url = "https://files.pythonhosted.org/packages/88/f0/efbfa391395cce2f2771f937e0620cfd185ec88f2b9cd88711028a768e96/mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67", size = 12772068, upload-time = "2025-12-15T05:02:53.689Z" }, + { url = "https://files.pythonhosted.org/packages/25/05/58b3ba28f5aed10479e899a12d2120d582ba9fa6288851b20bf1c32cbb4f/mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e", size = 13520385, upload-time = "2025-12-15T05:02:38.328Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a0/c006ccaff50b31e542ae69b92fe7e2f55d99fba3a55e01067dd564325f85/mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376", size = 13796221, upload-time = "2025-12-15T05:03:22.147Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ff/8bdb051cd710f01b880472241bd36b3f817a8e1c5d5540d0b761675b6de2/mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24", size = 10055456, upload-time = "2025-12-15T05:03:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nh3" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/a5/34c26015d3a434409f4d2a1cd8821a06c05238703f49283ffeb937bef093/nh3-0.3.2.tar.gz", hash = "sha256:f394759a06df8b685a4ebfb1874fb67a9cbfd58c64fc5ed587a663c0e63ec376", size = 19288, upload-time = "2025-10-30T11:17:45.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/01/a1eda067c0ba823e5e2bb033864ae4854549e49fb6f3407d2da949106bfb/nh3-0.3.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d18957a90806d943d141cc5e4a0fefa1d77cf0d7a156878bf9a66eed52c9cc7d", size = 1419839, upload-time = "2025-10-30T11:17:09.956Z" }, + { url = "https://files.pythonhosted.org/packages/30/57/07826ff65d59e7e9cc789ef1dc405f660cabd7458a1864ab58aefa17411b/nh3-0.3.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45c953e57028c31d473d6b648552d9cab1efe20a42ad139d78e11d8f42a36130", size = 791183, upload-time = "2025-10-30T11:17:11.99Z" }, + { url = "https://files.pythonhosted.org/packages/af/2f/e8a86f861ad83f3bb5455f596d5c802e34fcdb8c53a489083a70fd301333/nh3-0.3.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c9850041b77a9147d6bbd6dbbf13eeec7009eb60b44e83f07fcb2910075bf9b", size = 829127, upload-time = "2025-10-30T11:17:13.192Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/77aef4daf0479754e8e90c7f8f48f3b7b8725a3b8c0df45f2258017a6895/nh3-0.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:403c11563e50b915d0efdb622866d1d9e4506bce590ef7da57789bf71dd148b5", size = 997131, upload-time = "2025-10-30T11:17:14.677Z" }, + { url = "https://files.pythonhosted.org/packages/41/ee/fd8140e4df9d52143e89951dd0d797f5546004c6043285289fbbe3112293/nh3-0.3.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0dca4365db62b2d71ff1620ee4f800c4729849906c5dd504ee1a7b2389558e31", size = 1068783, upload-time = "2025-10-30T11:17:15.861Z" }, + { url = "https://files.pythonhosted.org/packages/87/64/bdd9631779e2d588b08391f7555828f352e7f6427889daf2fa424bfc90c9/nh3-0.3.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0fe7ee035dd7b2290715baf29cb27167dddd2ff70ea7d052c958dbd80d323c99", size = 994732, upload-time = "2025-10-30T11:17:17.155Z" }, + { url = "https://files.pythonhosted.org/packages/79/66/90190033654f1f28ca98e3d76b8be1194505583f9426b0dcde782a3970a2/nh3-0.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a40202fd58e49129764f025bbaae77028e420f1d5b3c8e6f6fd3a6490d513868", size = 975997, upload-time = "2025-10-30T11:17:18.77Z" }, + { url = "https://files.pythonhosted.org/packages/34/30/ebf8e2e8d71fdb5a5d5d8836207177aed1682df819cbde7f42f16898946c/nh3-0.3.2-cp314-cp314t-win32.whl", hash = "sha256:1f9ba555a797dbdcd844b89523f29cdc90973d8bd2e836ea6b962cf567cadd93", size = 583364, upload-time = "2025-10-30T11:17:20.286Z" }, + { url = "https://files.pythonhosted.org/packages/94/ae/95c52b5a75da429f11ca8902c2128f64daafdc77758d370e4cc310ecda55/nh3-0.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:dce4248edc427c9b79261f3e6e2b3ecbdd9b88c267012168b4a7b3fc6fd41d13", size = 589982, upload-time = "2025-10-30T11:17:21.384Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bd/c7d862a4381b95f2469704de32c0ad419def0f4a84b7a138a79532238114/nh3-0.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:019ecbd007536b67fdf76fab411b648fb64e2257ca3262ec80c3425c24028c80", size = 577126, upload-time = "2025-10-30T11:17:22.755Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3e/f5a5cc2885c24be13e9b937441bd16a012ac34a657fe05e58927e8af8b7a/nh3-0.3.2-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7064ccf5ace75825bd7bf57859daaaf16ed28660c1c6b306b649a9eda4b54b1e", size = 1431980, upload-time = "2025-10-30T11:17:25.457Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f7/529a99324d7ef055de88b690858f4189379708abae92ace799365a797b7f/nh3-0.3.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8745454cdd28bbbc90861b80a0111a195b0e3961b9fa2e672be89eb199fa5d8", size = 820805, upload-time = "2025-10-30T11:17:26.98Z" }, + { url = "https://files.pythonhosted.org/packages/3d/62/19b7c50ccd1fa7d0764822d2cea8f2a320f2fd77474c7a1805cb22cf69b0/nh3-0.3.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72d67c25a84579f4a432c065e8b4274e53b7cf1df8f792cf846abfe2c3090866", size = 803527, upload-time = "2025-10-30T11:17:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ca/f022273bab5440abff6302731a49410c5ef66b1a9502ba3fbb2df998d9ff/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:13398e676a14d6233f372c75f52d5ae74f98210172991f7a3142a736bd92b131", size = 1051674, upload-time = "2025-10-30T11:17:29.909Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f7/5728e3b32a11daf5bd21cf71d91c463f74305938bc3eb9e0ac1ce141646e/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03d617e5c8aa7331bd2659c654e021caf9bba704b109e7b2b28b039a00949fe5", size = 1004737, upload-time = "2025-10-30T11:17:31.205Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/f17e0dba0a99cee29e6cee6d4d52340ef9cb1f8a06946d3a01eb7ec2fb01/nh3-0.3.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f55c4d2d5a207e74eefe4d828067bbb01300e06e2a7436142f915c5928de07", size = 911745, upload-time = "2025-10-30T11:17:32.945Z" }, + { url = "https://files.pythonhosted.org/packages/42/0f/c76bf3dba22c73c38e9b1113b017cf163f7696f50e003404ec5ecdb1e8a6/nh3-0.3.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb18403f02b655a1bbe4e3a4696c2ae1d6ae8f5991f7cacb684b1ae27e6c9f7", size = 797184, upload-time = "2025-10-30T11:17:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/08/a1/73d8250f888fb0ddf1b119b139c382f8903d8bb0c5bd1f64afc7e38dad1d/nh3-0.3.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d66f41672eb4060cf87c037f760bdbc6847852ca9ef8e9c5a5da18f090abf87", size = 838556, upload-time = "2025-10-30T11:17:35.875Z" }, + { url = "https://files.pythonhosted.org/packages/d1/09/deb57f1fb656a7a5192497f4a287b0ade5a2ff6b5d5de4736d13ef6d2c1f/nh3-0.3.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f97f8b25cb2681d25e2338148159447e4d689aafdccfcf19e61ff7db3905768a", size = 1006695, upload-time = "2025-10-30T11:17:37.071Z" }, + { url = "https://files.pythonhosted.org/packages/b6/61/8f4d41c4ccdac30e4b1a4fa7be4b0f9914d8314a5058472f84c8e101a418/nh3-0.3.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2ab70e8c6c7d2ce953d2a58102eefa90c2d0a5ed7aa40c7e29a487bc5e613131", size = 1075471, upload-time = "2025-10-30T11:17:38.225Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c6/966aec0cb4705e69f6c3580422c239205d5d4d0e50fac380b21e87b6cf1b/nh3-0.3.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1710f3901cd6440ca92494ba2eb6dc260f829fa8d9196b659fa10de825610ce0", size = 1002439, upload-time = "2025-10-30T11:17:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c8/97a2d5f7a314cce2c5c49f30c6f161b7f3617960ade4bfc2fd1ee092cb20/nh3-0.3.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91e9b001101fb4500a2aafe3e7c92928d85242d38bf5ac0aba0b7480da0a4cd6", size = 987439, upload-time = "2025-10-30T11:17:40.81Z" }, + { url = "https://files.pythonhosted.org/packages/0d/95/2d6fc6461687d7a171f087995247dec33e8749a562bfadd85fb5dbf37a11/nh3-0.3.2-cp38-abi3-win32.whl", hash = "sha256:169db03df90da63286e0560ea0efa9b6f3b59844a9735514a1d47e6bb2c8c61b", size = 589826, upload-time = "2025-10-30T11:17:42.239Z" }, + { url = "https://files.pythonhosted.org/packages/64/9a/1a1c154f10a575d20dd634e5697805e589bbdb7673a0ad00e8da90044ba7/nh3-0.3.2-cp38-abi3-win_amd64.whl", hash = "sha256:562da3dca7a17f9077593214a9781a94b8d76de4f158f8c895e62f09573945fe", size = 596406, upload-time = "2025-10-30T11:17:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7e/a96255f63b7aef032cbee8fc4d6e37def72e3aaedc1f72759235e8f13cb1/nh3-0.3.2-cp38-abi3-win_arm64.whl", hash = "sha256:cf5964d54edd405e68583114a7cba929468bcd7db5e676ae38ee954de1cfc104", size = 584162, upload-time = "2025-10-30T11:17:44.96Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "paho-mqtt" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/15/0a6214e76d4d32e7f663b109cf71fb22561c2be0f701d67f93950cd40542/paho_mqtt-2.1.0.tar.gz", hash = "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834", size = 148848, upload-time = "2024-04-29T19:52:55.591Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/cb/00451c3cf31790287768bb12c6bec834f5d292eaf3022afc88e14b8afc94/paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", size = 67219, upload-time = "2024-04-29T19:52:48.345Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, +] + +[[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 = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pluggy", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pluggy", marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, + { name = "coverage", version = "7.13.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "pluggy" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[package]] +name = "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/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, + { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, +] + +[[package]] +name = "readme-renderer" +version = "44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "nh3" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056, upload-time = "2024-07-08T15:00:57.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310, upload-time = "2024-07-08T15:00:56.577Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rfc3986" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/77/9a7fe084d268f8855d493e5031ea03fa0af8cc05887f638bf1c4e3363eb8/ruff-0.14.11.tar.gz", hash = "sha256:f6dc463bfa5c07a59b1ff2c3b9767373e541346ea105503b4c0369c520a66958", size = 5993417, upload-time = "2026-01-08T19:11:58.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/a6/a4c40a5aaa7e331f245d2dc1ac8ece306681f52b636b40ef87c88b9f7afd/ruff-0.14.11-py3-none-linux_armv6l.whl", hash = "sha256:f6ff2d95cbd335841a7217bdfd9c1d2e44eac2c584197ab1385579d55ff8830e", size = 12951208, upload-time = "2026-01-08T19:12:09.218Z" }, + { url = "https://files.pythonhosted.org/packages/5c/5c/360a35cb7204b328b685d3129c08aca24765ff92b5a7efedbdd6c150d555/ruff-0.14.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f6eb5c1c8033680f4172ea9c8d3706c156223010b8b97b05e82c59bdc774ee6", size = 13330075, upload-time = "2026-01-08T19:12:02.549Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9e/0cc2f1be7a7d33cae541824cf3f95b4ff40d03557b575912b5b70273c9ec/ruff-0.14.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2fc34cc896f90080fca01259f96c566f74069a04b25b6205d55379d12a6855e", size = 12257809, upload-time = "2026-01-08T19:12:00.366Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e5/5faab97c15bb75228d9f74637e775d26ac703cc2b4898564c01ab3637c02/ruff-0.14.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53386375001773ae812b43205d6064dae49ff0968774e6befe16a994fc233caa", size = 12678447, upload-time = "2026-01-08T19:12:13.899Z" }, + { url = "https://files.pythonhosted.org/packages/1b/33/e9767f60a2bef779fb5855cab0af76c488e0ce90f7bb7b8a45c8a2ba4178/ruff-0.14.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a697737dce1ca97a0a55b5ff0434ee7205943d4874d638fe3ae66166ff46edbe", size = 12758560, upload-time = "2026-01-08T19:11:42.55Z" }, + { url = "https://files.pythonhosted.org/packages/eb/84/4c6cf627a21462bb5102f7be2a320b084228ff26e105510cd2255ea868e5/ruff-0.14.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6845ca1da8ab81ab1dce755a32ad13f1db72e7fba27c486d5d90d65e04d17b8f", size = 13599296, upload-time = "2026-01-08T19:11:30.371Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/92b5ed7ea66d849f6157e695dc23d5d6d982bd6aa8d077895652c38a7cae/ruff-0.14.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e36ce2fd31b54065ec6f76cb08d60159e1b32bdf08507862e32f47e6dde8bcbf", size = 15048981, upload-time = "2026-01-08T19:12:04.742Z" }, + { url = "https://files.pythonhosted.org/packages/61/df/c1bd30992615ac17c2fb64b8a7376ca22c04a70555b5d05b8f717163cf9f/ruff-0.14.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590bcc0e2097ecf74e62a5c10a6b71f008ad82eb97b0a0079e85defe19fe74d9", size = 14633183, upload-time = "2026-01-08T19:11:40.069Z" }, + { url = "https://files.pythonhosted.org/packages/04/e9/fe552902f25013dd28a5428a42347d9ad20c4b534834a325a28305747d64/ruff-0.14.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53fe71125fc158210d57fe4da26e622c9c294022988d08d9347ec1cf782adafe", size = 14050453, upload-time = "2026-01-08T19:11:37.555Z" }, + { url = "https://files.pythonhosted.org/packages/ae/93/f36d89fa021543187f98991609ce6e47e24f35f008dfe1af01379d248a41/ruff-0.14.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3", size = 13757889, upload-time = "2026-01-08T19:12:07.094Z" }, + { url = "https://files.pythonhosted.org/packages/b7/9f/c7fb6ecf554f28709a6a1f2a7f74750d400979e8cd47ed29feeaa1bd4db8/ruff-0.14.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0f3727189a52179393ecf92ec7057c2210203e6af2676f08d92140d3e1ee72c1", size = 13955832, upload-time = "2026-01-08T19:11:55.064Z" }, + { url = "https://files.pythonhosted.org/packages/db/a0/153315310f250f76900a98278cf878c64dfb6d044e184491dd3289796734/ruff-0.14.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eb09f849bd37147a789b85995ff734a6c4a095bed5fd1608c4f56afc3634cde2", size = 12586522, upload-time = "2026-01-08T19:11:35.356Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2b/a73a2b6e6d2df1d74bf2b78098be1572191e54bec0e59e29382d13c3adc5/ruff-0.14.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c61782543c1231bf71041461c1f28c64b961d457d0f238ac388e2ab173d7ecb7", size = 12724637, upload-time = "2026-01-08T19:11:47.796Z" }, + { url = "https://files.pythonhosted.org/packages/f0/41/09100590320394401cd3c48fc718a8ba71c7ddb1ffd07e0ad6576b3a3df2/ruff-0.14.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82ff352ea68fb6766140381748e1f67f83c39860b6446966cff48a315c3e2491", size = 13145837, upload-time = "2026-01-08T19:11:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d8/e035db859d1d3edf909381eb8ff3e89a672d6572e9454093538fe6f164b0/ruff-0.14.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:728e56879df4ca5b62a9dde2dd0eb0edda2a55160c0ea28c4025f18c03f86984", size = 13850469, upload-time = "2026-01-08T19:12:11.694Z" }, + { url = "https://files.pythonhosted.org/packages/4e/02/bb3ff8b6e6d02ce9e3740f4c17dfbbfb55f34c789c139e9cd91985f356c7/ruff-0.14.11-py3-none-win32.whl", hash = "sha256:337c5dd11f16ee52ae217757d9b82a26400be7efac883e9e852646f1557ed841", size = 12851094, upload-time = "2026-01-08T19:11:45.163Z" }, + { url = "https://files.pythonhosted.org/packages/58/f1/90ddc533918d3a2ad628bc3044cdfc094949e6d4b929220c3f0eb8a1c998/ruff-0.14.11-py3-none-win_amd64.whl", hash = "sha256:f981cea63d08456b2c070e64b79cb62f951aa1305282974d4d5216e6e0178ae6", size = 14001379, upload-time = "2026-01-08T19:11:52.591Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "cryptography", marker = "python_full_version < '3.10'" }, + { name = "jeepney", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739, upload-time = "2022-08-13T16:22:46.976Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221, upload-time = "2022-08-13T16:22:44.457Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "cryptography", marker = "python_full_version >= '3.10'" }, + { name = "jeepney", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + +[[package]] +name = "symphony-sdk" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "pyyaml" }, + { name = "requests" }, +] + +[package.optional-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest-cov" }, + { name = "ruff" }, + { name = "types-pyyaml" }, + { name = "types-requests" }, +] +mqtt = [ + { name = "paho-mqtt" }, +] + +[package.dev-dependencies] +dev = [ + { name = "symphony-sdk" }, + { name = "twine" }, +] + +[package.metadata] +requires-dist = [ + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0" }, + { name = "paho-mqtt", marker = "extra == 'mqtt'", specifier = ">=2.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0" }, + { name = "pyyaml", specifier = ">=6.0" }, + { name = "requests", specifier = ">=2.25" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, + { name = "types-pyyaml", marker = "extra == 'dev'", specifier = ">=6.0" }, + { name = "types-requests", marker = "extra == 'dev'", specifier = ">=2.25" }, +] +provides-extras = ["mqtt", "dev"] + +[package.metadata.requires-dev] +dev = [ + { name = "symphony-sdk", editable = "." }, + { name = "twine", specifier = ">=6.2.0" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "twine" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "id" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, + { name = "packaging" }, + { name = "readme-renderer" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/a8/949edebe3a82774c1ec34f637f5dd82d1cf22c25e963b7d63771083bbee5/twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf", size = 172262, upload-time = "2025-09-04T15:43:17.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl", hash = "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", size = 42727, upload-time = "2025-09-04T15:43:15.994Z" }, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20250915" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]